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/>.
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.
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
67 :param dsa1: A DSA object
68 :param dsa2: Another DSA
69 :return: -1, 0, or 1, indicating sort order.
71 if dsa1.is_gc() and not dsa2.is_gc():
73 if not dsa1.is_gc() and dsa2.is_gc():
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)
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_file_dir: write diagnostic Graphviz files in this directory
102 def __init__(self, unix_now, readonly=False, verify=False, debug=False,
104 """Initializes the partitions class which can hold
105 our local DCs partitions or all the partitions in
108 self.part_table = {} # partition objects
110 self.ip_transport = None
111 self.sitelink_table = {}
112 self.dsa_by_dnstr = {}
113 self.dsa_by_guid = {}
115 self.get_dsa_by_guidstr = self.dsa_by_guid.get
116 self.get_dsa = self.dsa_by_dnstr.get
118 # TODO: These should be backed by a 'permanent' store so that when
119 # calling DRSGetReplInfo with DS_REPL_INFO_KCC_DSA_CONNECT_FAILURES,
120 # the failure information can be returned
121 self.kcc_failed_links = {}
122 self.kcc_failed_connections = set()
124 # Used in inter-site topology computation. A list
125 # of connections (by NTDSConnection object) that are
126 # to be kept when pruning un-needed NTDS Connections
127 self.kept_connections = set()
129 self.my_dsa_dnstr = None # My dsa DN
130 self.my_dsa = None # My dsa object
132 self.my_site_dnstr = None
137 self.unix_now = unix_now
138 self.nt_now = unix2nttime(unix_now)
139 self.readonly = readonly
142 self.dot_file_dir = dot_file_dir
144 def load_ip_transport(self):
145 """Loads the inter-site transport objects for Sites
148 :raise KCCError: if no IP transport is found
151 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
152 self.samdb.get_config_basedn(),
153 scope=ldb.SCOPE_SUBTREE,
154 expression="(objectClass=interSiteTransport)")
155 except ldb.LdbError, (enum, estr):
156 raise KCCError("Unable to find inter-site transports - (%s)" %
162 transport = Transport(dnstr)
164 transport.load_transport(self.samdb)
165 if transport.name == 'IP':
166 self.ip_transport = transport
167 elif transport.name == 'SMTP':
168 logger.info("Samba KCC is ignoring the obsolete SMTP transport.")
171 logger.warning("Samba KCC does not support the transport called %r."
174 if self.ip_transport is None:
175 raise KCCError("there doesn't seem to be an IP transport")
177 def load_all_sitelinks(self):
178 """Loads the inter-site siteLink objects
181 :raise KCCError: if site-links aren't found
184 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
185 self.samdb.get_config_basedn(),
186 scope=ldb.SCOPE_SUBTREE,
187 expression="(objectClass=siteLink)")
188 except ldb.LdbError, (enum, estr):
189 raise KCCError("Unable to find inter-site siteLinks - (%s)" % estr)
195 if dnstr in self.sitelink_table:
198 sitelink = SiteLink(dnstr)
200 sitelink.load_sitelink(self.samdb)
202 # Assign this siteLink to table
204 self.sitelink_table[dnstr] = sitelink
206 def load_site(self, dn_str):
207 """Helper for load_my_site and load_all_sites.
209 Put all the site's DSAs into the KCC indices.
211 :param dn_str: a site dn_str
212 :return: the Site object pertaining to the dn_str
214 site = Site(dn_str, self.unix_now)
215 site.load_site(self.samdb)
217 # We avoid replacing the site with an identical copy in case
218 # somewhere else has a reference to the old one, which would
219 # lead to all manner of confusion and chaos.
220 guid = str(site.site_guid)
221 if guid not in self.site_table:
222 self.site_table[guid] = site
223 self.dsa_by_dnstr.update(site.dsa_table)
224 self.dsa_by_guid.update((str(x.dsa_guid), x)
225 for x in site.dsa_table.values())
227 return self.site_table[guid]
229 def load_my_site(self):
230 """Load the Site object for the local DSA.
234 self.my_site_dnstr = ("CN=%s,CN=Sites,%s" % (
235 self.samdb.server_site_name(),
236 self.samdb.get_config_basedn()))
238 self.my_site = self.load_site(self.my_site_dnstr)
240 def load_all_sites(self):
241 """Discover all sites and create Site objects.
244 :raise: KCCError if sites can't be found
247 res = self.samdb.search("CN=Sites,%s" %
248 self.samdb.get_config_basedn(),
249 scope=ldb.SCOPE_SUBTREE,
250 expression="(objectClass=site)")
251 except ldb.LdbError, (enum, estr):
252 raise KCCError("Unable to find sites - (%s)" % estr)
255 sitestr = str(msg.dn)
256 self.load_site(sitestr)
258 def load_my_dsa(self):
259 """Discover my nTDSDSA dn thru the rootDSE entry
262 :raise: KCCError if DSA can't be found
264 dn = ldb.Dn(self.samdb, "<GUID=%s>" % self.samdb.get_ntds_GUID())
266 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
267 attrs=["objectGUID"])
268 except ldb.LdbError, (enum, estr):
269 logger.warning("Search for %s failed: %s. This typically happens"
270 " in --importldif mode due to lack of module"
271 " support.", dn, estr)
273 # We work around the failure above by looking at the
274 # dsServiceName that was put in the fake rootdse by
275 # the --exportldif, rather than the
276 # samdb.get_ntds_GUID(). The disadvantage is that this
277 # mode requires we modify the @ROOTDSE dnq to support
279 service_name_res = self.samdb.search(base="",
280 scope=ldb.SCOPE_BASE,
281 attrs=["dsServiceName"])
282 dn = ldb.Dn(self.samdb,
283 service_name_res[0]["dsServiceName"][0])
285 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
286 attrs=["objectGUID"])
287 except ldb.LdbError, (enum, estr):
288 raise KCCError("Unable to find my nTDSDSA - (%s)" % estr)
291 raise KCCError("Unable to find my nTDSDSA at %s" %
294 ntds_guid = misc.GUID(self.samdb.get_ntds_GUID())
295 if misc.GUID(res[0]["objectGUID"][0]) != ntds_guid:
296 raise KCCError("Did not find the GUID we expected,"
297 " perhaps due to --importldif")
299 self.my_dsa_dnstr = str(res[0].dn)
301 self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
303 if self.my_dsa_dnstr not in self.dsa_by_dnstr:
304 debug.DEBUG_DARK_YELLOW("my_dsa %s isn't in self.dsas_by_dnstr:"
305 " it must be RODC.\n"
306 "Let's add it, because my_dsa is special!"
307 "\n(likewise for self.dsa_by_guid)" %
310 self.dsa_by_dnstr[self.my_dsa_dnstr] = self.my_dsa
311 self.dsa_by_guid[str(self.my_dsa.dsa_guid)] = self.my_dsa
313 def load_all_partitions(self):
314 """Discover and load all partitions.
316 Each NC is inserted into the part_table by partition
317 dn string (not the nCName dn string)
320 :raise: KCCError if partitions can't be found
323 res = self.samdb.search("CN=Partitions,%s" %
324 self.samdb.get_config_basedn(),
325 scope=ldb.SCOPE_SUBTREE,
326 expression="(objectClass=crossRef)")
327 except ldb.LdbError, (enum, estr):
328 raise KCCError("Unable to find partitions - (%s)" % estr)
331 partstr = str(msg.dn)
334 if partstr in self.part_table:
337 part = Partition(partstr)
339 part.load_partition(self.samdb)
340 self.part_table[partstr] = part
342 def refresh_failed_links_connections(self, ping=None):
343 """Ensure the failed links list is up to date
345 Based on MS-ADTS 6.2.2.1
347 :param ping: An oracle function of remote site availability
350 # LINKS: Refresh failed links
351 self.kcc_failed_links = {}
352 current, needed = self.my_dsa.get_rep_tables()
353 for replica in current.values():
354 # For every possible connection to replicate
355 for reps_from in replica.rep_repsFrom:
356 failure_count = reps_from.consecutive_sync_failures
357 if failure_count <= 0:
360 dsa_guid = str(reps_from.source_dsa_obj_guid)
361 time_first_failure = reps_from.last_success
362 last_result = reps_from.last_attempt
363 dns_name = reps_from.dns_name1
365 f = self.kcc_failed_links.get(dsa_guid)
367 f = KCCFailedObject(dsa_guid, failure_count,
368 time_first_failure, last_result,
370 self.kcc_failed_links[dsa_guid] = f
372 f.failure_count = max(f.failure_count, failure_count)
373 f.time_first_failure = min(f.time_first_failure,
375 f.last_result = last_result
377 # CONNECTIONS: Refresh failed connections
378 restore_connections = set()
380 DEBUG("refresh_failed_links: checking if links are still down")
381 for connection in self.kcc_failed_connections:
382 if ping(connection.dns_name):
383 # Failed connection is no longer failing
384 restore_connections.add(connection)
386 connection.failure_count += 1
388 DEBUG("refresh_failed_links: not checking live links because we\n"
389 "weren't asked to --attempt-live-connections")
391 # Remove the restored connections from the failed connections
392 self.kcc_failed_connections.difference_update(restore_connections)
394 def is_stale_link_connection(self, target_dsa):
395 """Check whether a link to a remote DSA is stale
397 Used in MS-ADTS 6.2.2.2 Intrasite Connection Creation
399 Returns True if the remote seems to have been down for at
400 least two hours, otherwise False.
402 :param target_dsa: the remote DSA object
403 :return: True if link is stale, otherwise False
405 failed_link = self.kcc_failed_links.get(str(target_dsa.dsa_guid))
407 # failure_count should be > 0, but check anyways
408 if failed_link.failure_count > 0:
409 unix_first_failure = \
410 nttime2unix(failed_link.time_first_failure)
411 # TODO guard against future
412 if unix_first_failure > self.unix_now:
413 logger.error("The last success time attribute for \
414 repsFrom is in the future!")
416 # Perform calculation in seconds
417 if (self.unix_now - unix_first_failure) > 60 * 60 * 2:
421 # We have checked failed *links*, but we also need to check
426 # TODO: This should be backed by some form of local database
427 def remove_unneeded_failed_links_connections(self):
428 # Remove all tuples in kcc_failed_links where failure count = 0
429 # In this implementation, this should never happen.
431 # Remove all connections which were not used this run or connections
432 # that became active during this run.
435 def remove_unneeded_ntdsconn(self, all_connected):
436 """Remove unneeded NTDS Connections once topology is calculated
438 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections
440 :param all_connected: indicates whether all sites are connected
445 # New connections won't have GUIDs which are needed for
447 for cn_conn in mydsa.connect_table.values():
448 if cn_conn.guid is None:
450 cn_conn.guid = misc.GUID(str(uuid.uuid4()))
451 cn_conn.whenCreated = self.nt_now
453 cn_conn.load_connection(self.samdb)
455 for cn_conn in mydsa.connect_table.values():
457 s_dnstr = cn_conn.get_from_dnstr()
459 cn_conn.to_be_deleted = True
462 #XXX should an RODC be regarded as same site
463 same_site = s_dnstr in self.my_site.dsa_table
465 # Given an nTDSConnection object cn, if the DC with the
466 # nTDSDSA object dc that is the parent object of cn and
467 # the DC with the nTDSDA object referenced by cn!fromServer
468 # are in the same site, the KCC on dc deletes cn if all of
469 # the following are true:
471 # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
473 # No site settings object s exists for the local DC's site, or
474 # bit NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED is clear in
477 # Another nTDSConnection object cn2 exists such that cn and
478 # cn2 have the same parent object, cn!fromServer = cn2!fromServer,
481 # cn!whenCreated < cn2!whenCreated
483 # cn!whenCreated = cn2!whenCreated and
484 # cn!objectGUID < cn2!objectGUID
486 # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
488 if not cn_conn.is_generated():
491 if self.my_site.is_cleanup_ntdsconn_disabled():
494 # Loop thru connections looking for a duplicate that
495 # fulfills the previous criteria
497 packed_guid = ndr_pack(cn_conn.guid)
498 for cn2_conn in mydsa.connect_table.values():
499 if cn2_conn is cn_conn:
502 s2_dnstr = cn2_conn.get_from_dnstr()
504 # If the NTDS Connections has a different
505 # fromServer field then no match
506 if s2_dnstr != s_dnstr:
509 lesser = (cn_conn.whenCreated < cn2_conn.whenCreated or
510 (cn_conn.whenCreated == cn2_conn.whenCreated and
511 packed_guid < ndr_pack(cn2_conn.guid)))
516 if lesser and not cn_conn.is_rodc_topology():
517 cn_conn.to_be_deleted = True
519 # Given an nTDSConnection object cn, if the DC with the nTDSDSA
520 # object dc that is the parent object of cn and the DC with
521 # the nTDSDSA object referenced by cn!fromServer are in
522 # different sites, a KCC acting as an ISTG in dc's site
523 # deletes cn if all of the following are true:
525 # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
527 # cn!fromServer references an nTDSDSA object for a DC
528 # in a site other than the local DC's site.
530 # The keepConnections sequence returned by
531 # CreateIntersiteConnections() does not contain
532 # cn!objectGUID, or cn is "superseded by" (see below)
533 # another nTDSConnection cn2 and keepConnections
534 # contains cn2!objectGUID.
536 # The return value of CreateIntersiteConnections()
539 # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in
542 else: # different site
544 if not mydsa.is_istg():
547 if not cn_conn.is_generated():
551 # We are directly using this connection in intersite or
552 # we are using a connection which can supersede this one.
554 # MS-ADTS 6.2.2.4 - Removing Unnecessary Connections does not
555 # appear to be correct.
557 # 1. cn!fromServer and cn!parent appear inconsistent with
559 # 2. The repsFrom do not imply each other
561 if cn_conn in self.kept_connections: # and not_superceded:
564 # This is the result of create_intersite_connections
565 if not all_connected:
568 if not cn_conn.is_rodc_topology():
569 cn_conn.to_be_deleted = True
571 if mydsa.is_ro() or self.readonly:
572 for connect in mydsa.connect_table.values():
573 if connect.to_be_deleted:
574 DEBUG_FN("TO BE DELETED:\n%s" % connect)
575 if connect.to_be_added:
576 DEBUG_FN("TO BE ADDED:\n%s" % connect)
578 # Peform deletion from our tables but perform
579 # no database modification
580 mydsa.commit_connections(self.samdb, ro=True)
582 # Commit any modified connections
583 mydsa.commit_connections(self.samdb)
585 def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
586 """Update an repsFrom object if required.
588 Part of MS-ADTS 6.2.2.5.
590 Update t_repsFrom if necessary to satisfy requirements. Such
591 updates are typically required when the IDL_DRSGetNCChanges
592 server has moved from one site to another--for example, to
593 enable compression when the server is moved from the
594 client's site to another site.
596 The repsFrom.update_flags bit field may be modified
597 auto-magically if any changes are made here. See
598 kcc_utils.RepsFromTo for gory details.
601 :param n_rep: NC replica we need
602 :param t_repsFrom: repsFrom tuple to modify
603 :param s_rep: NC replica at source DSA
604 :param s_dsa: source DSA
605 :param cn_conn: Local DSA NTDSConnection child
609 s_dnstr = s_dsa.dsa_dnstr
610 same_site = s_dnstr in self.my_site.dsa_table
612 # if schedule doesn't match then update and modify
613 times = convert_schedule_to_repltimes(cn_conn.schedule)
614 if times != t_repsFrom.schedule:
615 t_repsFrom.schedule = times
617 # Bit DRS_PER_SYNC is set in replicaFlags if and only
618 # if nTDSConnection schedule has a value v that specifies
619 # scheduled replication is to be performed at least once
621 if cn_conn.is_schedule_minimum_once_per_week():
623 if ((t_repsFrom.replica_flags &
624 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0):
625 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
627 # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
628 # if the source DSA and the local DC's nTDSDSA object are
629 # in the same site or source dsa is the FSMO role owner
630 # of one or more FSMO roles in the NC replica.
631 if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
633 if ((t_repsFrom.replica_flags &
634 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0):
635 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
637 # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
638 # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
639 # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
640 # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
641 # t.replicaFlags if and only if s and the local DC's
642 # nTDSDSA object are in different sites.
643 if ((cn_conn.options &
644 dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0):
646 if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
649 # it LOOKS as if this next test is a bit silly: it
650 # checks the flag then sets it if it not set; the same
651 # effect could be achieved by unconditionally setting
652 # it. But in fact the repsFrom object has special
653 # magic attached to it, and altering replica_flags has
654 # side-effects. That is bad in my opinion, but there
656 if ((t_repsFrom.replica_flags &
657 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
658 t_repsFrom.replica_flags |= \
659 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
663 if ((t_repsFrom.replica_flags &
664 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
665 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
667 # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
668 # and only if s and the local DC's nTDSDSA object are
669 # not in the same site and the
670 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
671 # clear in cn!options
672 if (not same_site and
674 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
676 if ((t_repsFrom.replica_flags &
677 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0):
678 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
680 # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
681 # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
682 if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
684 if ((t_repsFrom.replica_flags &
685 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0):
686 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
688 # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
689 # set in t.replicaFlags if and only if cn!enabledConnection = false.
690 if not cn_conn.is_enabled():
692 if ((t_repsFrom.replica_flags &
693 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0):
694 t_repsFrom.replica_flags |= \
695 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
697 if ((t_repsFrom.replica_flags &
698 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0):
699 t_repsFrom.replica_flags |= \
700 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
702 # If s and the local DC's nTDSDSA object are in the same site,
703 # cn!transportType has no value, or the RDN of cn!transportType
706 # Bit DRS_MAIL_REP in t.replicaFlags is clear.
708 # t.uuidTransport = NULL GUID.
710 # t.uuidDsa = The GUID-based DNS name of s.
714 # Bit DRS_MAIL_REP in t.replicaFlags is set.
716 # If x is the object with dsname cn!transportType,
717 # t.uuidTransport = x!objectGUID.
719 # Let a be the attribute identified by
720 # x!transportAddressAttribute. If a is
721 # the dNSHostName attribute, t.uuidDsa = the GUID-based
722 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
724 # It appears that the first statement i.e.
726 # "If s and the local DC's nTDSDSA object are in the same
727 # site, cn!transportType has no value, or the RDN of
728 # cn!transportType is CN=IP:"
730 # could be a slightly tighter statement if it had an "or"
731 # between each condition. I believe this should
734 # IF (same-site) OR (no-value) OR (type-ip)
736 # because IP should be the primary transport mechanism
737 # (even in inter-site) and the absense of the transportType
738 # attribute should always imply IP no matter if its multi-site
740 # NOTE MS-TECH INCORRECT:
742 # All indications point to these statements above being
743 # incorrectly stated:
745 # t.uuidDsa = The GUID-based DNS name of s.
747 # Let a be the attribute identified by
748 # x!transportAddressAttribute. If a is
749 # the dNSHostName attribute, t.uuidDsa = the GUID-based
750 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
752 # because the uuidDSA is a GUID and not a GUID-base DNS
753 # name. Nor can uuidDsa hold (s!parent)!a if not
754 # dNSHostName. What should have been said is:
756 # t.naDsa = The GUID-based DNS name of s
758 # That would also be correct if transportAddressAttribute
759 # were "mailAddress" because (naDsa) can also correctly
760 # hold the SMTP ISM service address.
762 nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
764 if ((t_repsFrom.replica_flags &
765 drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0):
766 t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
768 t_repsFrom.transport_guid = misc.GUID()
770 # See (NOTE MS-TECH INCORRECT) above
772 # NOTE: it looks like these conditionals are pointless,
773 # because the state will end up as `t_repsFrom.dns_name1 ==
774 # nastr` in either case, BUT the repsFrom thing is magic and
775 # assigning to it alters some flags. So we try not to update
776 # it unless necessary.
777 if t_repsFrom.dns_name1 != nastr:
778 t_repsFrom.dns_name1 = nastr
780 if t_repsFrom.version > 0x1 and t_repsFrom.dns_name2 != nastr:
781 t_repsFrom.dns_name2 = nastr
783 if t_repsFrom.is_modified():
784 DEBUG_FN("modify_repsFrom(): %s" % t_repsFrom)
786 def get_dsa_for_implied_replica(self, n_rep, cn_conn):
787 """If a connection imply a replica, find the relevant DSA
789 Given a NC replica and NTDS Connection, determine if the
790 connection implies a repsFrom tuple should be present from the
791 source DSA listed in the connection to the naming context. If
792 it should be, return the DSA; otherwise return None.
794 Based on part of MS-ADTS 6.2.2.5
796 :param n_rep: NC replica
797 :param cn_conn: NTDS Connection
798 :return: source DSA or None
800 #XXX different conditions for "implies" than MS-ADTS 6.2.2
802 # NTDS Connection must satisfy all the following criteria
803 # to imply a repsFrom tuple is needed:
805 # cn!enabledConnection = true.
806 # cn!options does not contain NTDSCONN_OPT_RODC_TOPOLOGY.
807 # cn!fromServer references an nTDSDSA object.
809 if not cn_conn.is_enabled() or cn_conn.is_rodc_topology():
812 s_dnstr = cn_conn.get_from_dnstr()
813 s_dsa = self.get_dsa(s_dnstr)
815 # No DSA matching this source DN string?
819 # To imply a repsFrom tuple is needed, each of these
822 # An NC replica of the NC "is present" on the DC to
823 # which the nTDSDSA object referenced by cn!fromServer
826 # An NC replica of the NC "should be present" on
828 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
830 if s_rep is None or not s_rep.is_present():
833 # To imply a repsFrom tuple is needed, each of these
836 # The NC replica on the DC referenced by cn!fromServer is
837 # a writable replica or the NC replica that "should be
838 # present" on the local DC is a partial replica.
840 # The NC is not a domain NC, the NC replica that
841 # "should be present" on the local DC is a partial
842 # replica, cn!transportType has no value, or
843 # cn!transportType has an RDN of CN=IP.
845 implied = (not s_rep.is_ro() or n_rep.is_partial()) and \
846 (not n_rep.is_domain() or
847 n_rep.is_partial() or
848 cn_conn.transport_dnstr is None or
849 cn_conn.transport_dnstr.find("CN=IP") == 0)
855 def translate_ntdsconn(self, current_dsa=None):
856 """Adjust repsFrom to match NTDSConnections
858 This function adjusts values of repsFrom abstract attributes of NC
859 replicas on the local DC to match those implied by
860 nTDSConnection objects.
862 Based on [MS-ADTS] 6.2.2.5
864 :param current_dsa: optional DSA on whose behalf we are acting.
869 if current_dsa is None:
870 current_dsa = self.my_dsa
872 if current_dsa.is_translate_ntdsconn_disabled():
873 DEBUG_FN("skipping translate_ntdsconn() "
874 "because disabling flag is set")
877 DEBUG_FN("translate_ntdsconn(): enter")
879 current_rep_table, needed_rep_table = current_dsa.get_rep_tables()
881 # Filled in with replicas we currently have that need deleting
884 # We're using the MS notation names here to allow
885 # correlation back to the published algorithm.
887 # n_rep - NC replica (n)
888 # t_repsFrom - tuple (t) in n!repsFrom
889 # s_dsa - Source DSA of the replica. Defined as nTDSDSA
890 # object (s) such that (s!objectGUID = t.uuidDsa)
891 # In our IDL representation of repsFrom the (uuidDsa)
892 # attribute is called (source_dsa_obj_guid)
893 # cn_conn - (cn) is nTDSConnection object and child of the local
894 # DC's nTDSDSA object and (cn!fromServer = s)
895 # s_rep - source DSA replica of n
897 # If we have the replica and its not needed
898 # then we add it to the "to be deleted" list.
899 for dnstr in current_rep_table:
900 if dnstr not in needed_rep_table:
901 delete_reps.add(dnstr)
903 DEBUG_FN('current %d needed %d delete %d' % (len(current_rep_table),
904 len(needed_rep_table), len(delete_reps)))
907 DEBUG('deleting these reps: %s' % delete_reps)
908 for dnstr in delete_reps:
909 del current_rep_table[dnstr]
911 # Now perform the scan of replicas we'll need
912 # and compare any current repsFrom against the
914 for n_rep in needed_rep_table.values():
916 # load any repsFrom and fsmo roles as we'll
917 # need them during connection translation
918 n_rep.load_repsFrom(self.samdb)
919 n_rep.load_fsmo_roles(self.samdb)
921 # Loop thru the existing repsFrom tupples (if any)
922 # XXX This is a list and could contain duplicates
923 # (multiple load_repsFrom calls)
924 for t_repsFrom in n_rep.rep_repsFrom:
926 # for each tuple t in n!repsFrom, let s be the nTDSDSA
927 # object such that s!objectGUID = t.uuidDsa
928 guidstr = str(t_repsFrom.source_dsa_obj_guid)
929 s_dsa = self.get_dsa_by_guidstr(guidstr)
931 # Source dsa is gone from config (strange)
932 # so cleanup stale repsFrom for unlisted DSA
934 logger.warning("repsFrom source DSA guid (%s) not found" %
936 t_repsFrom.to_be_deleted = True
939 # Find the connection that this repsFrom would use. If
940 # there isn't a good one (i.e. non-RODC_TOPOLOGY,
941 # meaning non-FRS), we delete the repsFrom.
942 s_dnstr = s_dsa.dsa_dnstr
943 connections = current_dsa.get_connection_by_from_dnstr(s_dnstr)
944 for cn_conn in connections:
945 if not cn_conn.is_rodc_topology():
948 # no break means no non-rodc_topology connection exists
949 t_repsFrom.to_be_deleted = True
952 # KCC removes this repsFrom tuple if any of the following
954 # No NC replica of the NC "is present" on DSA that
955 # would be source of replica
957 # A writable replica of the NC "should be present" on
958 # the local DC, but a partial replica "is present" on
960 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
962 if s_rep is None or not s_rep.is_present() or \
963 (not n_rep.is_ro() and s_rep.is_partial()):
965 t_repsFrom.to_be_deleted = True
968 # If the KCC did not remove t from n!repsFrom, it updates t
969 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
971 # Loop thru connections and add implied repsFrom tuples
972 # for each NTDSConnection under our local DSA if the
973 # repsFrom is not already present
974 for cn_conn in current_dsa.connect_table.values():
976 s_dsa = self.get_dsa_for_implied_replica(n_rep, cn_conn)
980 # Loop thru the existing repsFrom tupples (if any) and
981 # if we already have a tuple for this connection then
982 # no need to proceed to add. It will have been changed
983 # to have the correct attributes above
984 for t_repsFrom in n_rep.rep_repsFrom:
985 guidstr = str(t_repsFrom.source_dsa_obj_guid)
986 if s_dsa is self.get_dsa_by_guidstr(guidstr):
993 # Create a new RepsFromTo and proceed to modify
994 # it according to specification
995 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
997 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
999 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
1001 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
1003 # Add to our NC repsFrom as this is newly computed
1004 if t_repsFrom.is_modified():
1005 n_rep.rep_repsFrom.append(t_repsFrom)
1008 # Display any to be deleted or modified repsFrom
1009 text = n_rep.dumpstr_to_be_deleted()
1011 logger.info("TO BE DELETED:\n%s" % text)
1012 text = n_rep.dumpstr_to_be_modified()
1014 logger.info("TO BE MODIFIED:\n%s" % text)
1016 # Peform deletion from our tables but perform
1017 # no database modification
1018 n_rep.commit_repsFrom(self.samdb, ro=True)
1020 # Commit any modified repsFrom to the NC replica
1021 n_rep.commit_repsFrom(self.samdb)
1023 def merge_failed_links(self, ping=None):
1024 """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
1026 The KCC on a writable DC attempts to merge the link and connection
1027 failure information from bridgehead DCs in its own site to help it
1028 identify failed bridgehead DCs.
1030 Based on MS-ADTS 6.2.2.3.2 "Merge of kCCFailedLinks and kCCFailedLinks
1033 :param ping: An oracle of current bridgehead availability
1036 # 1. Queries every bridgehead server in your site (other than yourself)
1037 # 2. For every ntDSConnection that references a server in a different
1038 # site merge all the failure info
1040 # XXX - not implemented yet
1041 if ping is not None:
1042 debug.DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1044 DEBUG_FN("skipping merge_failed_links() because it requires "
1045 "real network connections\n"
1046 "and we weren't asked to --attempt-live-connections")
1048 def setup_graph(self, part):
1049 """Set up an intersite graph
1051 An intersite graph has a Vertex for each site object, a
1052 MultiEdge for each SiteLink object, and a MutliEdgeSet for
1053 each siteLinkBridge object (or implied siteLinkBridge). It
1054 reflects the intersite topology in a slightly more abstract
1057 Roughly corresponds to MS-ADTS 6.2.2.3.4.3
1059 :param part: a Partition object
1060 :returns: an InterSiteGraph object
1062 # If 'Bridge all site links' is enabled and Win2k3 bridges required
1064 # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1065 # No documentation for this however, ntdsapi.h appears to have:
1066 # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1067 bridges_required = self.my_site.site_options & 0x00001002 != 0
1068 transport_guid = str(self.ip_transport.guid)
1070 g = setup_graph(part, self.site_table, transport_guid,
1071 self.sitelink_table, bridges_required)
1073 if self.verify or self.dot_file_dir is not None:
1075 for edge in g.edges:
1076 for a, b in itertools.combinations(edge.vertices, 2):
1077 dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1078 verify_properties = ()
1079 name = 'site_edges_%s' % part.partstr
1080 verify_and_dot(name, dot_edges, directed=False,
1081 label=self.my_dsa_dnstr,
1082 properties=verify_properties, debug=DEBUG,
1084 dot_file_dir=self.dot_file_dir)
1088 def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1089 """Get a bridghead DC for a site.
1091 Part of MS-ADTS 6.2.2.3.4.4
1093 :param site: site object representing for which a bridgehead
1095 :param part: crossRef for NC to replicate.
1096 :param transport: interSiteTransport object for replication
1098 :param partial_ok: True if a DC containing a partial
1099 replica or a full replica will suffice, False if only
1100 a full replica will suffice.
1101 :param detect_failed: True to detect failed DCs and route
1102 replication traffic around them, False to assume no DC
1104 :return: dsa object for the bridgehead DC or None
1107 bhs = self.get_all_bridgeheads(site, part, transport,
1108 partial_ok, detect_failed)
1110 debug.DEBUG_MAGENTA("get_bridgehead:\n\tsitedn=%s\n\tbhdn=None" %
1114 debug.DEBUG_GREEN("get_bridgehead:\n\tsitedn=%s\n\tbhdn=%s" %
1115 (site.site_dnstr, bhs[0].dsa_dnstr))
1118 def get_all_bridgeheads(self, site, part, transport,
1119 partial_ok, detect_failed):
1120 """Get all bridghead DCs on a site satisfying the given criteria
1122 Part of MS-ADTS 6.2.2.3.4.4
1124 :param site: site object representing the site for which
1125 bridgehead DCs are desired.
1126 :param part: partition for NC to replicate.
1127 :param transport: interSiteTransport object for
1128 replication traffic.
1129 :param partial_ok: True if a DC containing a partial
1130 replica or a full replica will suffice, False if
1131 only a full replica will suffice.
1132 :param detect_failed: True to detect failed DCs and route
1133 replication traffic around them, FALSE to assume
1135 :return: list of dsa object for available bridgehead DCs
1139 if transport.name != "IP":
1140 raise KCCError("get_all_bridgeheads has run into a "
1141 "non-IP transport! %r"
1142 % (transport.name,))
1144 DEBUG_FN("get_all_bridgeheads")
1145 DEBUG_FN(site.rw_dsa_table)
1146 for dsa in site.rw_dsa_table.values():
1148 pdnstr = dsa.get_parent_dnstr()
1150 # IF t!bridgeheadServerListBL has one or more values and
1151 # t!bridgeheadServerListBL does not contain a reference
1152 # to the parent object of dc then skip dc
1153 if ((len(transport.bridgehead_list) != 0 and
1154 pdnstr not in transport.bridgehead_list)):
1157 # IF dc is in the same site as the local DC
1158 # IF a replica of cr!nCName is not in the set of NC replicas
1159 # that "should be present" on dc or a partial replica of the
1160 # NC "should be present" but partialReplicasOkay = FALSE
1162 if self.my_site.same_site(dsa):
1163 needed, ro, partial = part.should_be_present(dsa)
1164 if not needed or (partial and not partial_ok):
1166 rep = dsa.get_current_replica(part.nc_dnstr)
1169 # IF an NC replica of cr!nCName is not in the set of NC
1170 # replicas that "are present" on dc or a partial replica of
1171 # the NC "is present" but partialReplicasOkay = FALSE
1174 rep = dsa.get_current_replica(part.nc_dnstr)
1175 if rep is None or (rep.is_partial() and not partial_ok):
1178 # IF AmIRODC() and cr!nCName corresponds to default NC then
1179 # Let dsaobj be the nTDSDSA object of the dc
1180 # IF dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1182 if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1183 if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1186 # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1188 if self.is_bridgehead_failed(dsa, detect_failed):
1189 DEBUG("bridgehead is failed")
1192 DEBUG_FN("get_all_bridgeheads: dsadn=%s" % dsa.dsa_dnstr)
1195 # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1197 # SORT bhs such that all GC servers precede DCs that are not GC
1198 # servers, and otherwise by ascending objectGUID
1200 # SORT bhs in a random order
1201 if site.is_random_bridgehead_disabled():
1202 bhs.sort(sort_dsa_by_gc_and_guid)
1205 debug.DEBUG_YELLOW(bhs)
1208 def is_bridgehead_failed(self, dsa, detect_failed):
1209 """Determine whether a given DC is known to be in a failed state
1211 :param dsa: the bridgehead to test
1212 :param detect_failed: True to really check, False to assume no failure
1213 :return: True if and only if the DC should be considered failed
1215 Here we DEPART from the pseudo code spec which appears to be
1216 wrong. It says, in full:
1218 /***** BridgeheadDCFailed *****/
1219 /* Determine whether a given DC is known to be in a failed state.
1220 * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1221 * IN: detectFailedDCs - TRUE if and only failed DC detection is
1223 * RETURNS: TRUE if and only if the DC should be considered to be in a
1226 BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1228 IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1229 the options attribute of the site settings object for the local
1232 ELSEIF a tuple z exists in the kCCFailedLinks or
1233 kCCFailedConnections variables such that z.UUIDDsa =
1234 objectGUID, z.FailureCount > 1, and the current time -
1235 z.TimeFirstFailure > 2 hours
1238 RETURN detectFailedDCs
1242 where you will see detectFailedDCs is not behaving as
1243 advertised -- it is acting as a default return code in the
1244 event that a failure is not detected, not a switch turning
1245 detection on or off. Elsewhere the documentation seems to
1246 concur with the comment rather than the code.
1248 if not detect_failed:
1251 # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1252 # When DETECT_STALE_DISABLED, we can never know of if
1253 # it's in a failed state
1254 if self.my_site.site_options & 0x00000008:
1257 return self.is_stale_link_connection(dsa)
1259 def create_connection(self, part, rbh, rsite, transport,
1260 lbh, lsite, link_opt, link_sched,
1261 partial_ok, detect_failed):
1262 """Create an nTDSConnection object as specified if it doesn't exist.
1264 Part of MS-ADTS 6.2.2.3.4.5
1266 :param part: crossRef object for the NC to replicate.
1267 :param rbh: nTDSDSA object for DC to act as the
1268 IDL_DRSGetNCChanges server (which is in a site other
1269 than the local DC's site).
1270 :param rsite: site of the rbh
1271 :param transport: interSiteTransport object for the transport
1272 to use for replication traffic.
1273 :param lbh: nTDSDSA object for DC to act as the
1274 IDL_DRSGetNCChanges client (which is in the local DC's site).
1275 :param lsite: site of the lbh
1276 :param link_opt: Replication parameters (aggregated siteLink options,
1278 :param link_sched: Schedule specifying the times at which
1279 to begin replicating.
1280 :partial_ok: True if bridgehead DCs containing partial
1281 replicas of the NC are acceptable.
1282 :param detect_failed: True to detect failed DCs and route
1283 replication traffic around them, FALSE to assume no DC
1286 rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1288 rbh_table = dict((x.dsa_dnstr, x) for x in rbhs_all)
1290 debug.DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all),
1291 [x.dsa_dnstr for x in rbhs_all]))
1293 # MS-TECH says to compute rbhs_avail but then doesn't use it
1294 # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1295 # partial_ok, detect_failed)
1297 lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1300 lbhs_all.append(lbh)
1302 debug.DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all),
1303 [x.dsa_dnstr for x in lbhs_all]))
1305 # MS-TECH says to compute lbhs_avail but then doesn't use it
1306 # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1307 # partial_ok, detect_failed)
1309 # FOR each nTDSConnection object cn such that the parent of cn is
1310 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1311 for ldsa in lbhs_all:
1312 for cn in ldsa.connect_table.values():
1314 rdsa = rbh_table.get(cn.from_dnstr)
1318 debug.DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1319 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1320 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1321 # cn!transportType references t
1322 if ((cn.is_generated() and
1323 not cn.is_rodc_topology() and
1324 cn.transport_guid == transport.guid)):
1326 # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1327 # cn!options and cn!schedule != sch
1328 # Perform an originating update to set cn!schedule to
1330 if ((not cn.is_user_owned_schedule() and
1331 not cn.is_equivalent_schedule(link_sched))):
1332 cn.schedule = link_sched
1333 cn.set_modified(True)
1335 # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1336 # NTDSCONN_OPT_USE_NOTIFY are set in cn
1337 if cn.is_override_notify_default() and \
1340 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1342 # Perform an originating update to clear bits
1343 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1344 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1345 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1347 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1348 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1349 cn.set_modified(True)
1354 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1356 # Perform an originating update to set bits
1357 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1358 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1359 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1361 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1362 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1363 cn.set_modified(True)
1365 # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1366 if cn.is_twoway_sync():
1368 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1370 # Perform an originating update to clear bit
1371 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1372 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1373 cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1374 cn.set_modified(True)
1379 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1381 # Perform an originating update to set bit
1382 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1383 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1384 cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1385 cn.set_modified(True)
1387 # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1389 if cn.is_intersite_compression_disabled():
1391 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1393 # Perform an originating update to clear bit
1394 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1397 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0):
1399 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1400 cn.set_modified(True)
1404 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1406 # Perform an originating update to set bit
1407 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1410 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1412 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1413 cn.set_modified(True)
1415 # Display any modified connection
1417 if cn.to_be_modified:
1418 logger.info("TO BE MODIFIED:\n%s" % cn)
1420 ldsa.commit_connections(self.samdb, ro=True)
1422 ldsa.commit_connections(self.samdb)
1425 valid_connections = 0
1427 # FOR each nTDSConnection object cn such that cn!parent is
1428 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1429 for ldsa in lbhs_all:
1430 for cn in ldsa.connect_table.values():
1432 rdsa = rbh_table.get(cn.from_dnstr)
1436 debug.DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1438 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1439 # cn!transportType references t) and
1440 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1441 if (((not cn.is_generated() or
1442 cn.transport_guid == transport.guid) and
1443 not cn.is_rodc_topology())):
1445 # LET rguid be the objectGUID of the nTDSDSA object
1446 # referenced by cn!fromServer
1447 # LET lguid be (cn!parent)!objectGUID
1449 # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1450 # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1451 # Increment cValidConnections by 1
1452 if ((not self.is_bridgehead_failed(rdsa, detect_failed) and
1453 not self.is_bridgehead_failed(ldsa, detect_failed))):
1454 valid_connections += 1
1456 # IF keepConnections does not contain cn!objectGUID
1457 # APPEND cn!objectGUID to keepConnections
1458 self.kept_connections.add(cn)
1461 debug.DEBUG_RED("valid connections %d" % valid_connections)
1462 DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1463 # IF cValidConnections = 0
1464 if valid_connections == 0:
1466 # LET opt be NTDSCONN_OPT_IS_GENERATED
1467 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1469 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1470 # SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1471 # NTDSCONN_OPT_USE_NOTIFY in opt
1472 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1473 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1474 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1476 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1477 # SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1478 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1479 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1481 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1483 # SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1485 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1486 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1488 # Perform an originating update to create a new nTDSConnection
1489 # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1490 # cn!options = opt, cn!transportType is a reference to t,
1491 # cn!fromServer is a reference to rbh, and cn!schedule = sch
1492 DEBUG_FN("new connection, KCC dsa: %s" % self.my_dsa.dsa_dnstr)
1493 cn = lbh.new_connection(opt, 0, transport,
1494 rbh.dsa_dnstr, link_sched)
1496 # Display any added connection
1499 logger.info("TO BE ADDED:\n%s" % cn)
1501 lbh.commit_connections(self.samdb, ro=True)
1503 lbh.commit_connections(self.samdb)
1505 # APPEND cn!objectGUID to keepConnections
1506 self.kept_connections.add(cn)
1508 def add_transports(self, vertex, local_vertex, graph, detect_failed):
1509 """Build a Vertex's transport lists
1511 Each vertex has accept_red_red and accept_black lists that
1512 list what transports they accept under various conditions. The
1513 only transport that is ever accepted is IP, and a dummy extra
1514 transport called "EDGE_TYPE_ALL".
1516 Part of MS-ADTS 6.2.2.3.4.3 -- ColorVertices
1518 :param vertex: the remote vertex we are thinking about
1519 :param local_vertex: the vertex relating to the local site.
1520 :param graph: the intersite graph
1521 :param detect_failed: whether to detect failed links
1522 :return: True if some bridgeheads were not found
1524 # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1525 # here, but using vertex seems to make more sense. That is,
1526 # the docs want this:
1528 #bh = self.get_bridgehead(vertex.site, vertex.part, transport,
1529 # local_vertex.is_black(), detect_failed)
1533 vertex.accept_red_red = []
1534 vertex.accept_black = []
1535 found_failed = False
1537 if vertex in graph.connected_vertices:
1538 t_guid = str(self.ip_transport.guid)
1540 bh = self.get_bridgehead(vertex.site, vertex.part,
1542 vertex.is_black(), detect_failed)
1544 if vertex.site.is_rodc_site():
1545 vertex.accept_red_red.append(t_guid)
1549 vertex.accept_red_red.append(t_guid)
1550 vertex.accept_black.append(t_guid)
1552 # Add additional transport to ensure another run of Dijkstra
1553 vertex.accept_red_red.append("EDGE_TYPE_ALL")
1554 vertex.accept_black.append("EDGE_TYPE_ALL")
1558 def create_connections(self, graph, part, detect_failed):
1559 """Create intersite NTDSConnections as needed by a partition
1561 Construct an NC replica graph for the NC identified by
1562 the given crossRef, then create any additional nTDSConnection
1565 :param graph: site graph.
1566 :param part: crossRef object for NC.
1567 :param detect_failed: True to detect failed DCs and route
1568 replication traffic around them, False to assume no DC
1571 Modifies self.kept_connections by adding any connections
1572 deemed to be "in use".
1574 :return: (all_connected, found_failed_dc)
1575 (all_connected) True if the resulting NC replica graph
1576 connects all sites that need to be connected.
1577 (found_failed_dc) True if one or more failed DCs were
1580 all_connected = True
1581 found_failed = False
1583 DEBUG_FN("create_connections(): enter\n"
1584 "\tpartdn=%s\n\tdetect_failed=%s" %
1585 (part.nc_dnstr, detect_failed))
1587 # XXX - This is a highly abbreviated function from the MS-TECH
1588 # ref. It creates connections between bridgeheads to all
1589 # sites that have appropriate replicas. Thus we are not
1590 # creating a minimum cost spanning tree but instead
1591 # producing a fully connected tree. This should produce
1592 # a full (albeit not optimal cost) replication topology.
1594 my_vertex = Vertex(self.my_site, part)
1595 my_vertex.color_vertex()
1597 for v in graph.vertices:
1599 if self.add_transports(v, my_vertex, graph, False):
1602 # No NC replicas for this NC in the site of the local DC,
1603 # so no nTDSConnection objects need be created
1604 if my_vertex.is_white():
1605 return all_connected, found_failed
1607 edge_list, n_components = get_spanning_tree_edges(graph,
1611 DEBUG_FN("%s Number of components: %d" %
1612 (part.nc_dnstr, n_components))
1613 if n_components > 1:
1614 all_connected = False
1616 # LET partialReplicaOkay be TRUE if and only if
1617 # localSiteVertex.Color = COLOR.BLACK
1618 partial_ok = my_vertex.is_black()
1620 # Utilize the IP transport only for now
1621 transport = self.ip_transport
1623 DEBUG("edge_list %s" % edge_list)
1625 # XXX more accurate comparison?
1626 if e.directed and e.vertices[0].site is self.my_site:
1629 if e.vertices[0].site is self.my_site:
1630 rsite = e.vertices[1].site
1632 rsite = e.vertices[0].site
1634 # We don't make connections to our own site as that
1635 # is intrasite topology generator's job
1636 if rsite is self.my_site:
1637 DEBUG("rsite is my_site")
1640 # Determine bridgehead server in remote site
1641 rbh = self.get_bridgehead(rsite, part, transport,
1642 partial_ok, detect_failed)
1646 # RODC acts as an BH for itself
1648 # LET lbh be the nTDSDSA object of the local DC
1650 # LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1651 # cr, t, partialReplicaOkay, detectFailedDCs)
1652 if self.my_dsa.is_ro():
1653 lsite = self.my_site
1656 lsite = self.my_site
1657 lbh = self.get_bridgehead(lsite, part, transport,
1658 partial_ok, detect_failed)
1661 debug.DEBUG_RED("DISASTER! lbh is None")
1664 DEBUG_FN("lsite: %s\nrsite: %s" % (lsite, rsite))
1665 DEBUG_FN("vertices %s" % (e.vertices,))
1666 debug.DEBUG_BLUE("bridgeheads\n%s\n%s\n%s" % (lbh, rbh, "-" * 70))
1668 sitelink = e.site_link
1669 if sitelink is None:
1673 link_opt = sitelink.options
1674 link_sched = sitelink.schedule
1676 self.create_connection(part, rbh, rsite, transport,
1677 lbh, lsite, link_opt, link_sched,
1678 partial_ok, detect_failed)
1680 return all_connected, found_failed
1682 def create_intersite_connections(self):
1683 """Create NTDSConnections as necessary for all partitions.
1685 Computes an NC replica graph for each NC replica that "should be
1686 present" on the local DC or "is present" on any DC in the same site
1687 as the local DC. For each edge directed to an NC replica on such a
1688 DC from an NC replica on a DC in another site, the KCC creates an
1689 nTDSConnection object to imply that edge if one does not already
1692 Modifies self.kept_connections - A set of nTDSConnection
1693 objects for edges that are directed
1694 to the local DC's site in one or more NC replica graphs.
1696 :return: True if spanning trees were created for all NC replica
1697 graphs, otherwise False.
1699 all_connected = True
1700 self.kept_connections = set()
1702 # LET crossRefList be the set containing each object o of class
1703 # crossRef such that o is a child of the CN=Partitions child of the
1706 # FOR each crossRef object cr in crossRefList
1707 # IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1708 # is clear in cr!systemFlags, skip cr.
1709 # LET g be the GRAPH return of SetupGraph()
1711 for part in self.part_table.values():
1713 if not part.is_enabled():
1716 if part.is_foreign():
1719 graph = self.setup_graph(part)
1721 # Create nTDSConnection objects, routing replication traffic
1722 # around "failed" DCs.
1723 found_failed = False
1725 connected, found_failed = self.create_connections(graph,
1728 DEBUG("with detect_failed: connected %s Found failed %s" %
1729 (connected, found_failed))
1731 all_connected = False
1734 # One or more failed DCs preclude use of the ideal NC
1735 # replica graph. Add connections for the ideal graph.
1736 self.create_connections(graph, part, False)
1738 return all_connected
1740 def intersite(self, ping):
1741 """Generate the inter-site KCC replica graph and nTDSConnections
1743 As per MS-ADTS 6.2.2.3.
1745 If self.readonly is False, the connections are added to self.samdb.
1747 Produces self.kept_connections which is a set of NTDS
1748 Connections that should be kept during subsequent pruning
1751 After this has run, all sites should be connected in a minimum
1754 :param ping: An oracle function of remote site availability
1755 :return (True or False): (True) if the produced NC replica
1756 graph connects all sites that need to be connected
1761 mysite = self.my_site
1762 all_connected = True
1764 DEBUG_FN("intersite(): enter")
1766 # Determine who is the ISTG
1768 mysite.select_istg(self.samdb, mydsa, ro=True)
1770 mysite.select_istg(self.samdb, mydsa, ro=False)
1772 # Test whether local site has topology disabled
1773 if mysite.is_intersite_topology_disabled():
1774 DEBUG_FN("intersite(): exit disabled all_connected=%d" %
1776 return all_connected
1778 if not mydsa.is_istg():
1779 DEBUG_FN("intersite(): exit not istg all_connected=%d" %
1781 return all_connected
1783 self.merge_failed_links(ping)
1785 # For each NC with an NC replica that "should be present" on the
1786 # local DC or "is present" on any DC in the same site as the
1787 # local DC, the KCC constructs a site graph--a precursor to an NC
1788 # replica graph. The site connectivity for a site graph is defined
1789 # by objects of class interSiteTransport, siteLink, and
1790 # siteLinkBridge in the config NC.
1792 all_connected = self.create_intersite_connections()
1794 DEBUG_FN("intersite(): exit all_connected=%d" % all_connected)
1795 return all_connected
1797 def update_rodc_connection(self):
1798 """Updates the RODC NTFRS connection object.
1800 If the local DSA is not an RODC, this does nothing.
1802 if not self.my_dsa.is_ro():
1805 # Given an nTDSConnection object cn1, such that cn1.options contains
1806 # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1807 # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1808 # that the following is true:
1810 # cn1.fromServer = cn2.fromServer
1811 # cn1.schedule = cn2.schedule
1813 # If no such cn2 can be found, cn1 is not modified.
1814 # If no such cn1 can be found, nothing is modified by this task.
1816 all_connections = self.my_dsa.connect_table.values()
1817 ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1818 rw_connections = [x for x in all_connections
1819 if x not in ro_connections]
1821 # XXX here we are dealing with multiple RODC_TOPO connections,
1822 # if they exist. It is not clear whether the spec means that
1823 # or if it ever arises.
1824 if rw_connections and ro_connections:
1825 for con in ro_connections:
1826 cn2 = rw_connections[0]
1827 con.from_dnstr = cn2.from_dnstr
1828 con.schedule = cn2.schedule
1829 con.to_be_modified = True
1831 self.my_dsa.commit_connections(self.samdb, ro=self.readonly)
1833 def intrasite_max_node_edges(self, node_count):
1834 """Find the maximum number of edges directed to an intrasite node
1836 The KCC does not create more than 50 edges directed to a
1837 single DC. To optimize replication, we compute that each node
1838 should have n+2 total edges directed to it such that (n) is
1839 the smallest non-negative integer satisfying
1840 (node_count <= 2*(n*n) + 6*n + 7)
1842 (If the number of edges is m (i.e. n + 2), that is the same as
1843 2 * m*m - 2 * m + 3). We think in terms of n because that is
1844 the number of extra connections over the double directed ring
1845 that exists by default.
1855 :param node_count: total number of nodes in the replica graph
1857 The intention is that there should be no more than 3 hops
1858 between any two DSAs at a site. With up to 7 nodes the 2 edges
1859 of the ring are enough; any configuration of extra edges with
1860 8 nodes will be enough. It is less clear that the 3 hop
1861 guarantee holds at e.g. 15 nodes in degenerate cases, but
1862 those are quite unlikely given the extra edges are randomly
1865 :param node_count: the number of nodes in the site
1866 "return: The desired maximum number of connections
1870 if node_count <= (2 * (n * n) + (6 * n) + 7):
1878 def construct_intrasite_graph(self, site_local, dc_local,
1879 nc_x, gc_only, detect_stale):
1880 """Create an intrasite graph using given parameters
1882 This might be called a number of times per site with different
1885 Based on [MS-ADTS] 6.2.2.2
1887 :param site_local: site for which we are working
1888 :param dc_local: local DC that potentially needs a replica
1889 :param nc_x: naming context (x) that we are testing if it
1890 "should be present" on the local DC
1891 :param gc_only: Boolean - only consider global catalog servers
1892 :param detect_stale: Boolean - check whether links seems down
1895 # We're using the MS notation names here to allow
1896 # correlation back to the published algorithm.
1898 # nc_x - naming context (x) that we are testing if it
1899 # "should be present" on the local DC
1900 # f_of_x - replica (f) found on a DC (s) for NC (x)
1901 # dc_s - DC where f_of_x replica was found
1902 # dc_local - local DC that potentially needs a replica
1904 # r_list - replica list R
1905 # p_of_x - replica (p) is partial and found on a DC (s)
1907 # l_of_x - replica (l) is the local replica for NC (x)
1908 # that should appear on the local DC
1909 # r_len = is length of replica list |R|
1911 # If the DSA doesn't need a replica for this
1912 # partition (NC x) then continue
1913 needed, ro, partial = nc_x.should_be_present(dc_local)
1915 debug.DEBUG_YELLOW("construct_intrasite_graph(): enter" +
1916 "\n\tgc_only=%d" % gc_only +
1917 "\n\tdetect_stale=%d" % detect_stale +
1918 "\n\tneeded=%s" % needed +
1920 "\n\tpartial=%s" % partial +
1924 debug.DEBUG_RED("%s lacks 'should be present' status, "
1925 "aborting construct_intersite_graph!" %
1929 # Create a NCReplica that matches what the local replica
1930 # should say. We'll use this below in our r_list
1931 l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid,
1934 l_of_x.identify_by_basedn(self.samdb)
1936 l_of_x.rep_partial = partial
1939 # Add this replica that "should be present" to the
1940 # needed replica table for this DSA
1941 dc_local.add_needed_replica(l_of_x)
1945 # Let R be a sequence containing each writable replica f of x
1946 # such that f "is present" on a DC s satisfying the following
1949 # * s is a writable DC other than the local DC.
1951 # * s is in the same site as the local DC.
1953 # * If x is a read-only full replica and x is a domain NC,
1954 # then the DC's functional level is at least
1955 # DS_BEHAVIOR_WIN2008.
1957 # * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
1958 # in the options attribute of the site settings object for
1959 # the local DC's site, or no tuple z exists in the
1960 # kCCFailedLinks or kCCFailedConnections variables such
1961 # that z.UUIDDsa is the objectGUID of the nTDSDSA object
1962 # for s, z.FailureCount > 0, and the current time -
1963 # z.TimeFirstFailure > 2 hours.
1967 # We'll loop thru all the DSAs looking for
1968 # writeable NC replicas that match the naming
1969 # context dn for (nc_x)
1971 for dc_s in self.my_site.dsa_table.values():
1972 # If this partition (nc_x) doesn't appear as a
1973 # replica (f_of_x) on (dc_s) then continue
1974 if not nc_x.nc_dnstr in dc_s.current_rep_table:
1977 # Pull out the NCReplica (f) of (x) with the dn
1978 # that matches NC (x) we are examining.
1979 f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
1981 # Replica (f) of NC (x) must be writable
1985 # Replica (f) of NC (x) must satisfy the
1986 # "is present" criteria for DC (s) that
1988 if not f_of_x.is_present():
1991 # DC (s) must be a writable DSA other than
1992 # my local DC. In other words we'd only replicate
1993 # from other writable DC
1994 if dc_s.is_ro() or dc_s is dc_local:
1997 # Certain replica graphs are produced only
1998 # for global catalogs, so test against
1999 # method input parameter
2000 if gc_only and not dc_s.is_gc():
2003 # DC (s) must be in the same site as the local DC
2004 # as this is the intra-site algorithm. This is
2005 # handled by virtue of placing DSAs in per
2006 # site objects (see enclosing for() loop)
2008 # If NC (x) is intended to be read-only full replica
2009 # for a domain NC on the target DC then the source
2010 # DC should have functional level at minimum WIN2008
2012 # Effectively we're saying that in order to replicate
2013 # to a targeted RODC (which was introduced in Windows 2008)
2014 # then we have to replicate from a DC that is also minimally
2017 # You can also see this requirement in the MS special
2018 # considerations for RODC which state that to deploy
2019 # an RODC, at least one writable domain controller in
2020 # the domain must be running Windows Server 2008
2021 if ro and not partial and nc_x.nc_type == NCType.domain:
2022 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2025 # If we haven't been told to turn off stale connection
2026 # detection and this dsa has a stale connection then
2028 if detect_stale and self.is_stale_link_connection(dc_s):
2031 # Replica meets criteria. Add it to table indexed
2032 # by the GUID of the DC that it appears on
2033 r_list.append(f_of_x)
2035 # If a partial (not full) replica of NC (x) "should be present"
2036 # on the local DC, append to R each partial replica (p of x)
2037 # such that p "is present" on a DC satisfying the same
2038 # criteria defined above for full replica DCs.
2040 # XXX This loop and the previous one differ only in whether
2041 # the replica is partial or not. here we only accept partial
2042 # (because we're partial); before we only accepted full. Order
2043 # doen't matter (the list is sorted a few lines down) so these
2044 # loops could easily be merged. Or this could be a helper
2048 # Now we loop thru all the DSAs looking for
2049 # partial NC replicas that match the naming
2050 # context dn for (NC x)
2051 for dc_s in self.my_site.dsa_table.values():
2053 # If this partition NC (x) doesn't appear as a
2054 # replica (p) of NC (x) on the dsa DC (s) then
2056 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2059 # Pull out the NCReplica with the dn that
2060 # matches NC (x) we are examining.
2061 p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2063 # Replica (p) of NC (x) must be partial
2064 if not p_of_x.is_partial():
2067 # Replica (p) of NC (x) must satisfy the
2068 # "is present" criteria for DC (s) that
2070 if not p_of_x.is_present():
2073 # DC (s) must be a writable DSA other than
2074 # my DSA. In other words we'd only replicate
2075 # from other writable DSA
2076 if dc_s.is_ro() or dc_s is dc_local:
2079 # Certain replica graphs are produced only
2080 # for global catalogs, so test against
2081 # method input parameter
2082 if gc_only and not dc_s.is_gc():
2085 # If we haven't been told to turn off stale connection
2086 # detection and this dsa has a stale connection then
2088 if detect_stale and self.is_stale_link_connection(dc_s):
2091 # Replica meets criteria. Add it to table indexed
2092 # by the GUID of the DSA that it appears on
2093 r_list.append(p_of_x)
2095 # Append to R the NC replica that "should be present"
2097 r_list.append(l_of_x)
2099 r_list.sort(sort_replica_by_dsa_guid)
2102 max_node_edges = self.intrasite_max_node_edges(r_len)
2104 # Add a node for each r_list element to the replica graph
2107 node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2108 graph_list.append(node)
2110 # For each r(i) from (0 <= i < |R|-1)
2112 while i < (r_len-1):
2113 # Add an edge from r(i) to r(i+1) if r(i) is a full
2114 # replica or r(i+1) is a partial replica
2115 if not r_list[i].is_partial() or r_list[i+1].is_partial():
2116 graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
2118 # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2119 # replica or ri is a partial replica.
2120 if not r_list[i+1].is_partial() or r_list[i].is_partial():
2121 graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
2124 # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2125 # or r0 is a partial replica.
2126 if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
2127 graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
2129 # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2130 # r|R|-1 is a partial replica.
2131 if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
2132 graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
2134 DEBUG("r_list is length %s" % len(r_list))
2135 DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr))
2138 do_dot_files = self.dot_file_dir is not None and self.debug
2139 if self.verify or do_dot_files:
2141 dot_vertices = set()
2142 for v1 in graph_list:
2143 dot_vertices.add(v1.dsa_dnstr)
2144 for v2 in v1.edge_from:
2145 dot_edges.append((v2, v1.dsa_dnstr))
2146 dot_vertices.add(v2)
2148 verify_properties = ('connected',)
2149 verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2150 label='%s__%s__%s' % (site_local.site_dnstr,
2151 nctype_lut[nc_x.nc_type],
2153 properties=verify_properties, debug=DEBUG,
2155 dot_file_dir=self.dot_file_dir,
2158 rw_dot_vertices = set(x for x in dot_vertices
2159 if not self.get_dsa(x).is_ro())
2160 rw_dot_edges = [(a, b) for a, b in dot_edges if
2161 a in rw_dot_vertices and b in rw_dot_vertices]
2162 print rw_dot_edges, rw_dot_vertices
2163 rw_verify_properties = ('connected',
2164 'directed_double_ring_or_small')
2165 verify_and_dot('intrasite_rw_pre_ntdscon', rw_dot_edges,
2167 label='%s__%s__%s' % (site_local.site_dnstr,
2168 nctype_lut[nc_x.nc_type],
2170 properties=rw_verify_properties, debug=DEBUG,
2172 dot_file_dir=self.dot_file_dir,
2175 # For each existing nTDSConnection object implying an edge
2176 # from rj of R to ri such that j != i, an edge from rj to ri
2177 # is not already in the graph, and the total edges directed
2178 # to ri is less than n+2, the KCC adds that edge to the graph.
2179 for vertex in graph_list:
2180 dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2181 for connect in dsa.connect_table.values():
2182 remote = connect.from_dnstr
2183 if remote in self.my_site.dsa_table:
2184 vertex.add_edge_from(remote)
2186 DEBUG('reps are: %s' % ' '.join(x.rep_dsa_dnstr for x in r_list))
2187 DEBUG('dsas are: %s' % ' '.join(x.dsa_dnstr for x in graph_list))
2189 for tnode in graph_list:
2190 # To optimize replication latency in sites with many NC
2191 # replicas, the KCC adds new edges directed to ri to bring
2192 # the total edges to n+2, where the NC replica rk of R
2193 # from which the edge is directed is chosen at random such
2194 # that k != i and an edge from rk to ri is not already in
2197 # Note that the KCC tech ref does not give a number for
2198 # the definition of "sites with many NC replicas". At a
2199 # bare minimum to satisfy n+2 edges directed at a node we
2200 # have to have at least three replicas in |R| (i.e. if n
2201 # is zero then at least replicas from two other graph
2202 # nodes may direct edges to us).
2203 if r_len >= 3 and not tnode.has_sufficient_edges():
2204 candidates = [x for x in graph_list if
2206 x.dsa_dnstr not in tnode.edge_from)]
2208 debug.DEBUG_BLUE("looking for random link for %s. r_len %d, "
2209 "graph len %d candidates %d"
2210 % (tnode.dsa_dnstr, r_len, len(graph_list),
2213 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2215 while candidates and not tnode.has_sufficient_edges():
2216 other = random.choice(candidates)
2217 DEBUG("trying to add candidate %s" % other.dsa_dstr)
2218 if not tnode.add_edge_from(other):
2219 debug.DEBUG_RED("could not add %s" % other.dsa_dstr)
2220 candidates.remove(other)
2222 DEBUG_FN("not adding links to %s: nodes %s, links is %s/%s" %
2223 (tnode.dsa_dnstr, r_len, len(tnode.edge_from),
2226 # Print the graph node in debug mode
2227 DEBUG_FN("%s" % tnode)
2229 # For each edge directed to the local DC, ensure a nTDSConnection
2230 # points to us that satisfies the KCC criteria
2232 if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2233 tnode.add_connections_from_edges(dc_local)
2235 if self.verify or do_dot_files:
2237 dot_vertices = set()
2238 for v1 in graph_list:
2239 dot_vertices.add(v1.dsa_dnstr)
2240 for v2 in v1.edge_from:
2241 dot_edges.append((v2, v1.dsa_dnstr))
2242 dot_vertices.add(v2)
2244 verify_properties = ('connected',)
2245 verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2246 label='%s__%s__%s' % (site_local.site_dnstr,
2247 nctype_lut[nc_x.nc_type],
2249 properties=verify_properties, debug=DEBUG,
2251 dot_file_dir=self.dot_file_dir,
2254 rw_dot_vertices = set(x for x in dot_vertices
2255 if not self.get_dsa(x).is_ro())
2256 rw_dot_edges = [(a, b) for a, b in dot_edges if
2257 a in rw_dot_vertices and b in rw_dot_vertices]
2258 print rw_dot_edges, rw_dot_vertices
2259 rw_verify_properties = ('connected',
2260 'directed_double_ring_or_small')
2261 verify_and_dot('intrasite_rw_post_ntdscon', rw_dot_edges,
2263 label='%s__%s__%s' % (site_local.site_dnstr,
2264 nctype_lut[nc_x.nc_type],
2266 properties=rw_verify_properties, debug=DEBUG,
2268 dot_file_dir=self.dot_file_dir,
2271 def intrasite(self):
2272 """Generate the intrasite KCC connections
2274 As per MS-ADTS 6.2.2.2.
2276 If self.readonly is False, the connections are added to self.samdb.
2278 After this call, all DCs in each site with more than 3 DCs
2279 should be connected in a bidirectional ring. If a site has 2
2280 DCs, they will bidirectionally connected. Sites with many DCs
2281 may have arbitrary extra connections.
2287 DEBUG_FN("intrasite(): enter")
2289 # Test whether local site has topology disabled
2290 mysite = self.my_site
2291 if mysite.is_intrasite_topology_disabled():
2294 detect_stale = (not mysite.is_detect_stale_disabled())
2295 for connect in mydsa.connect_table.values():
2296 if connect.to_be_added:
2297 debug.DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2299 # Loop thru all the partitions, with gc_only False
2300 for partdn, part in self.part_table.items():
2301 self.construct_intrasite_graph(mysite, mydsa, part, False,
2303 for connect in mydsa.connect_table.values():
2304 if connect.to_be_added:
2305 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2307 # If the DC is a GC server, the KCC constructs an additional NC
2308 # replica graph (and creates nTDSConnection objects) for the
2309 # config NC as above, except that only NC replicas that "are present"
2310 # on GC servers are added to R.
2311 for connect in mydsa.connect_table.values():
2312 if connect.to_be_added:
2313 debug.DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2315 # Do it again, with gc_only True
2316 for partdn, part in self.part_table.items():
2317 if part.is_config():
2318 self.construct_intrasite_graph(mysite, mydsa, part, True,
2321 # The DC repeats the NC replica graph computation and nTDSConnection
2322 # creation for each of the NC replica graphs, this time assuming
2323 # that no DC has failed. It does so by re-executing the steps as
2324 # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2325 # set in the options attribute of the site settings object for
2326 # the local DC's site. (ie. we set "detec_stale" flag to False)
2327 for connect in mydsa.connect_table.values():
2328 if connect.to_be_added:
2329 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2331 # Loop thru all the partitions.
2332 for partdn, part in self.part_table.items():
2333 self.construct_intrasite_graph(mysite, mydsa, part, False,
2334 False) # don't detect stale
2336 # If the DC is a GC server, the KCC constructs an additional NC
2337 # replica graph (and creates nTDSConnection objects) for the
2338 # config NC as above, except that only NC replicas that "are present"
2339 # on GC servers are added to R.
2340 for connect in mydsa.connect_table.values():
2341 if connect.to_be_added:
2342 debug.DEBUG_RED("TO BE ADDED:\n%s" % connect)
2344 for partdn, part in self.part_table.items():
2345 if part.is_config():
2346 self.construct_intrasite_graph(mysite, mydsa, part, True,
2347 False) # don't detect stale
2350 # Display any to be added or modified repsFrom
2351 for connect in mydsa.connect_table.values():
2352 if connect.to_be_deleted:
2353 logger.info("TO BE DELETED:\n%s" % connect)
2354 if connect.to_be_modified:
2355 logger.info("TO BE MODIFIED:\n%s" % connect)
2356 if connect.to_be_added:
2357 debug.DEBUG_GREEN("TO BE ADDED:\n%s" % connect)
2359 mydsa.commit_connections(self.samdb, ro=True)
2361 # Commit any newly created connections to the samdb
2362 mydsa.commit_connections(self.samdb)
2364 def list_dsas(self):
2365 """Compile a comprehensive list of DSA DNs
2367 These are all the DSAs on all the sites that KCC would be
2370 This method is not idempotent and may not work correctly in
2371 sequence with KCC.run().
2373 :return: a list of DSA DN strings.
2378 self.load_all_sites()
2379 self.load_all_partitions()
2380 self.load_ip_transport()
2381 self.load_all_sitelinks()
2383 for site in self.site_table.values():
2384 dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2385 for dsa in site.dsa_table.values()])
2388 def load_samdb(self, dburl, lp, creds):
2389 """Load the database using an url, loadparm, and credentials
2391 :param dburl: a database url.
2392 :param lp: a loadparm object.
2393 :param creds: a Credentials object.
2395 self.samdb = SamDB(url=dburl,
2396 session_info=system_session(),
2397 credentials=creds, lp=lp)
2399 def plot_all_connections(self, basename, verify_properties=()):
2400 """Helper function to plot and verify NTDSConnections
2402 :param basename: an identifying string to use in filenames and logs.
2403 :param verify_properties: properties to verify (default empty)
2405 verify = verify_properties and self.verify
2406 if not verify and self.dot_file_dir is None:
2414 for dsa in self.dsa_by_dnstr.values():
2415 dot_vertices.append(dsa.dsa_dnstr)
2417 vertex_colours.append('#cc0000')
2419 vertex_colours.append('#0000cc')
2420 for con in dsa.connect_table.values():
2421 if con.is_rodc_topology():
2422 edge_colours.append('red')
2424 edge_colours.append('blue')
2425 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2427 verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2428 label=self.my_dsa_dnstr,
2429 properties=verify_properties, debug=DEBUG,
2430 verify=verify, dot_file_dir=self.dot_file_dir,
2431 directed=True, edge_colors=edge_colours,
2432 vertex_colors=vertex_colours)
2434 def run(self, dburl, lp, creds, forced_local_dsa=None,
2435 forget_local_links=False, forget_intersite_links=False,
2436 attempt_live_connections=False):
2437 """Perform a KCC run, possibly updating repsFrom topology
2439 :param dburl: url of the database to work with.
2440 :param lp: a loadparm object.
2441 :param creds: a Credentials object.
2442 :param forced_local_dsa: pretend to be on the DSA with this dn_str
2443 :param forget_local_links: calculate as if no connections existed
2444 (boolean, default False)
2445 :param forget_intersite_links: calculate with only intrasite connection
2446 (boolean, default False)
2447 :param attempt_live_connections: attempt to connect to remote DSAs to
2448 determine link availability (boolean, default False)
2449 :return: 1 on error, 0 otherwise
2451 # We may already have a samdb setup if we are
2452 # currently importing an ldif for a test run
2453 if self.samdb is None:
2455 self.load_samdb(dburl, lp, creds)
2456 except ldb.LdbError, (num, msg):
2457 logger.error("Unable to open sam database %s : %s" %
2461 if forced_local_dsa:
2462 self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" %
2470 self.load_all_sites()
2471 self.load_all_partitions()
2472 self.load_ip_transport()
2473 self.load_all_sitelinks()
2475 if self.verify or self.dot_file_dir is not None:
2477 for site in self.site_table.values():
2478 guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2480 in site.dsa_table.items())
2482 self.plot_all_connections('dsa_initial')
2485 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2486 for dnstr, c_rep in current_reps.items():
2487 DEBUG("c_rep %s" % c_rep)
2488 dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2490 verify_and_dot('dsa_repsFrom_initial', dot_edges,
2491 directed=True, label=self.my_dsa_dnstr,
2492 properties=(), debug=DEBUG, verify=self.verify,
2493 dot_file_dir=self.dot_file_dir)
2496 for site in self.site_table.values():
2497 for dsa in site.dsa_table.values():
2498 current_reps, needed_reps = dsa.get_rep_tables()
2499 for dn_str, rep in current_reps.items():
2500 for reps_from in rep.rep_repsFrom:
2501 DEBUG("rep %s" % rep)
2502 dsa_guid = str(reps_from.source_dsa_obj_guid)
2503 dsa_dn = guid_to_dnstr[dsa_guid]
2504 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2506 verify_and_dot('dsa_repsFrom_initial_all', dot_edges,
2507 directed=True, label=self.my_dsa_dnstr,
2508 properties=(), debug=DEBUG, verify=self.verify,
2509 dot_file_dir=self.dot_file_dir)
2512 for link in self.sitelink_table.values():
2513 for a, b in itertools.combinations(link.site_list, 2):
2514 dot_edges.append((str(a), str(b)))
2515 properties = ('connected',)
2516 verify_and_dot('dsa_sitelink_initial', dot_edges,
2518 label=self.my_dsa_dnstr, properties=properties,
2519 debug=DEBUG, verify=self.verify,
2520 dot_file_dir=self.dot_file_dir)
2522 if forget_local_links:
2523 for dsa in self.my_site.dsa_table.values():
2524 dsa.connect_table = dict((k, v) for k, v in
2525 dsa.connect_table.items()
2526 if v.is_rodc_topology())
2527 self.plot_all_connections('dsa_forgotten_local')
2529 if forget_intersite_links:
2530 for site in self.site_table.values():
2531 for dsa in site.dsa_table.values():
2532 dsa.connect_table = dict((k, v) for k, v in
2533 dsa.connect_table.items()
2534 if site is self.my_site and
2535 v.is_rodc_topology())
2537 self.plot_all_connections('dsa_forgotten_all')
2539 if attempt_live_connections:
2540 # Encapsulates lp and creds in a function that
2541 # attempts connections to remote DSAs.
2542 def ping(self, dnsname):
2544 drs_utils.drsuapi_connect(dnsname, self.lp, self.creds)
2545 except drs_utils.drsException:
2550 # These are the published steps (in order) for the
2551 # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2554 self.refresh_failed_links_connections(ping)
2560 all_connected = self.intersite(ping)
2563 self.remove_unneeded_ntdsconn(all_connected)
2566 self.translate_ntdsconn()
2569 self.remove_unneeded_failed_links_connections()
2572 self.update_rodc_connection()
2574 if self.verify or self.dot_file_dir is not None:
2575 self.plot_all_connections('dsa_final',
2578 debug.DEBUG_MAGENTA("there are %d dsa guids" %
2583 my_dnstr = self.my_dsa.dsa_dnstr
2584 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2585 for dnstr, n_rep in needed_reps.items():
2586 for reps_from in n_rep.rep_repsFrom:
2587 guid_str = str(reps_from.source_dsa_obj_guid)
2588 dot_edges.append((my_dnstr, guid_to_dnstr[guid_str]))
2589 edge_colors.append('#' + str(n_rep.nc_guid)[:6])
2591 verify_and_dot('dsa_repsFrom_final', dot_edges, directed=True,
2592 label=self.my_dsa_dnstr,
2593 properties=(), debug=DEBUG, verify=self.verify,
2594 dot_file_dir=self.dot_file_dir,
2595 edge_colors=edge_colors)
2599 for site in self.site_table.values():
2600 for dsa in site.dsa_table.values():
2601 current_reps, needed_reps = dsa.get_rep_tables()
2602 for n_rep in needed_reps.values():
2603 for reps_from in n_rep.rep_repsFrom:
2604 dsa_guid = str(reps_from.source_dsa_obj_guid)
2605 dsa_dn = guid_to_dnstr[dsa_guid]
2606 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2608 verify_and_dot('dsa_repsFrom_final_all', dot_edges,
2609 directed=True, label=self.my_dsa_dnstr,
2610 properties=(), debug=DEBUG, verify=self.verify,
2611 dot_file_dir=self.dot_file_dir)
2618 def import_ldif(self, dburl, lp, creds, ldif_file, forced_local_dsa=None):
2619 """Import relevant objects and attributes from an LDIF file.
2621 The point of this function is to allow a programmer/debugger to
2622 import an LDIF file with non-security relevent information that
2623 was previously extracted from a DC database. The LDIF file is used
2624 to create a temporary abbreviated database. The KCC algorithm can
2625 then run against this abbreviated database for debug or test
2626 verification that the topology generated is computationally the
2627 same between different OSes and algorithms.
2629 :param dburl: path to the temporary abbreviated db to create
2630 :param lp: a loadparm object.
2631 :param cred: a Credentials object.
2632 :param ldif_file: path to the ldif file to import
2633 :param forced_local_dsa: perform KCC from this DSA's point of view
2634 :return: zero on success, 1 on error
2637 self.samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif_file,
2639 except ldif_import_export.LdifError, e:
2644 def export_ldif(self, dburl, lp, creds, ldif_file):
2645 """Save KCC relevant details to an ldif file
2647 The point of this function is to allow a programmer/debugger to
2648 extract an LDIF file with non-security relevent information from
2649 a DC database. The LDIF file can then be used to "import" via
2650 the import_ldif() function this file into a temporary abbreviated
2651 database. The KCC algorithm can then run against this abbreviated
2652 database for debug or test verification that the topology generated
2653 is computationally the same between different OSes and algorithms.
2655 :param dburl: LDAP database URL to extract info from
2656 :param lp: a loadparm object.
2657 :param cred: a Credentials object.
2658 :param ldif_file: output LDIF file name to create
2659 :return: zero on success, 1 on error
2662 ldif_import_export.samdb_to_ldif_file(self.samdb, dburl, lp, creds,
2664 except ldif_import_export.LdifError, e: