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/>.
27 from samba import unix2nttime, nttime2unix
28 from samba import ldb, dsdb, drs_utils
29 from samba.auth import system_session
30 from samba.samdb import SamDB
31 from samba.dcerpc import drsuapi, misc
33 from samba.kcc.kcc_utils import Site, Partition, Transport, SiteLink
34 from samba.kcc.kcc_utils import NCReplica, NCType, nctype_lut, GraphNode
35 from samba.kcc.kcc_utils import RepsFromTo, KCCError, KCCFailedObject
36 from samba.kcc.graph import convert_schedule_to_repltimes
38 from samba.ndr import ndr_pack
40 from samba.kcc.graph_utils import verify_and_dot
42 from samba.kcc import ldif_import_export
43 from samba.kcc.graph import setup_graph, get_spanning_tree_edges
44 from samba.kcc.graph import Vertex
46 from samba.kcc.debug import DEBUG, DEBUG_FN, logger
47 from samba.kcc import debug
50 def sort_replica_by_dsa_guid(rep1, rep2):
51 """Helper to sort NCReplicas by their DSA guids
53 The guids need to be sorted in their NDR form.
55 :param rep1: An NC replica
56 :param rep2: Another replica
57 :return: -1, 0, or 1, indicating sort order.
59 return cmp(ndr_pack(rep1.rep_dsa_guid), ndr_pack(rep2.rep_dsa_guid))
62 def sort_dsa_by_gc_and_guid(dsa1, dsa2):
63 """Helper to sort DSAs by guid global catalog status
65 GC DSAs come before non-GC DSAs, other than that, the guids are
68 :param dsa1: A DSA object
69 :param dsa2: Another DSA
70 :return: -1, 0, or 1, indicating sort order.
72 if dsa1.is_gc() and not dsa2.is_gc():
74 if not dsa1.is_gc() and dsa2.is_gc():
76 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
79 def is_smtp_replication_available():
80 """Can the KCC use SMTP replication?
82 Currently always returns false because Samba doesn't implement
83 SMTP transfer for NC changes between DCs.
85 :return: Boolean (always False)
91 """The Knowledge Consistency Checker class.
93 A container for objects and methods allowing a run of the KCC. Produces a
94 set of connections in the samdb for which the Distributed Replication
95 Service can then utilize to replicate naming contexts
97 :param unix_now: The putative current time in seconds since 1970.
98 :param read_only: Don't write to the database.
99 :param verify: Check topological invariants for the generated graphs
100 :param debug: Write verbosely to stderr.
101 "param dot_file_dir: write diagnostic Graphviz files in this directory
103 def __init__(self, unix_now, readonly=False, verify=False, debug=False,
105 """Initializes the partitions class which can hold
106 our local DCs partitions or all the partitions in
109 self.part_table = {} # partition objects
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
138 self.unix_now = unix_now
139 self.nt_now = unix2nttime(unix_now)
140 self.readonly = readonly
143 self.dot_file_dir = dot_file_dir
145 def load_ip_transport(self):
146 """Loads the inter-site transport objects for Sites
149 :raise KCCError: if no IP transport is found
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)" %
163 transport = Transport(dnstr)
165 transport.load_transport(self.samdb)
166 if transport.name == 'IP':
167 self.ip_transport = transport
168 elif transport.name == 'SMTP':
169 logger.info("Samba KCC is ignoring the obsolete SMTP transport.")
172 logger.warning("Samba KCC does not support the transport called %r."
175 if self.ip_transport is None:
176 raise KCCError("there doesn't seem to be an IP transport")
178 def load_all_sitelinks(self):
179 """Loads the inter-site siteLink objects
182 :raise KCCError: if site-links aren't found
185 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
186 self.samdb.get_config_basedn(),
187 scope=ldb.SCOPE_SUBTREE,
188 expression="(objectClass=siteLink)")
189 except ldb.LdbError, (enum, estr):
190 raise KCCError("Unable to find inter-site siteLinks - (%s)" % estr)
196 if dnstr in self.sitelink_table:
199 sitelink = SiteLink(dnstr)
201 sitelink.load_sitelink(self.samdb)
203 # Assign this siteLink to table
205 self.sitelink_table[dnstr] = sitelink
207 def load_site(self, dn_str):
208 """Helper for load_my_site and load_all_sites.
210 Put all the site's DSAs into the KCC indices.
212 :param dn_str: a site dn_str
213 :return: the Site object pertaining to the dn_str
215 site = Site(dn_str, self.unix_now)
216 site.load_site(self.samdb)
218 # We avoid replacing the site with an identical copy in case
219 # somewhere else has a reference to the old one, which would
220 # lead to all manner of confusion and chaos.
221 guid = str(site.site_guid)
222 if guid not in self.site_table:
223 self.site_table[guid] = site
224 self.dsa_by_dnstr.update(site.dsa_table)
225 self.dsa_by_guid.update((str(x.dsa_guid), x)
226 for x in site.dsa_table.values())
228 return self.site_table[guid]
230 def load_my_site(self):
231 """Load the Site object for the local DSA.
235 self.my_site_dnstr = ("CN=%s,CN=Sites,%s" % (
236 self.samdb.server_site_name(),
237 self.samdb.get_config_basedn()))
239 self.my_site = self.load_site(self.my_site_dnstr)
241 def load_all_sites(self):
242 """Discover all sites and create Site objects.
245 :raise: KCCError if sites can't be found
248 res = self.samdb.search("CN=Sites,%s" %
249 self.samdb.get_config_basedn(),
250 scope=ldb.SCOPE_SUBTREE,
251 expression="(objectClass=site)")
252 except ldb.LdbError, (enum, estr):
253 raise KCCError("Unable to find sites - (%s)" % estr)
256 sitestr = str(msg.dn)
257 self.load_site(sitestr)
259 def load_my_dsa(self):
260 """Discover my nTDSDSA dn thru the rootDSE entry
263 :raise: KCCError if DSA can't be found
265 dn = ldb.Dn(self.samdb, "<GUID=%s>" % self.samdb.get_ntds_GUID())
267 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
268 attrs=["objectGUID"])
269 except ldb.LdbError, (enum, estr):
270 logger.warning("Search for %s failed: %s. This typically happens"
271 " in --importldif mode due to lack of module"
272 " support.", dn, estr)
274 # We work around the failure above by looking at the
275 # dsServiceName that was put in the fake rootdse by
276 # the --exportldif, rather than the
277 # samdb.get_ntds_GUID(). The disadvantage is that this
278 # mode requires we modify the @ROOTDSE dnq to support
280 service_name_res = self.samdb.search(base="",
281 scope=ldb.SCOPE_BASE,
282 attrs=["dsServiceName"])
283 dn = ldb.Dn(self.samdb,
284 service_name_res[0]["dsServiceName"][0])
286 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
287 attrs=["objectGUID"])
288 except ldb.LdbError, (enum, estr):
289 raise KCCError("Unable to find my nTDSDSA - (%s)" % estr)
292 raise KCCError("Unable to find my nTDSDSA at %s" %
295 ntds_guid = misc.GUID(self.samdb.get_ntds_GUID())
296 if misc.GUID(res[0]["objectGUID"][0]) != ntds_guid:
297 raise KCCError("Did not find the GUID we expected,"
298 " perhaps due to --importldif")
300 self.my_dsa_dnstr = str(res[0].dn)
302 self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
304 if self.my_dsa_dnstr not in self.dsa_by_dnstr:
305 debug.DEBUG_DARK_YELLOW("my_dsa %s isn't in self.dsas_by_dnstr:"
306 " it must be RODC.\n"
307 "Let's add it, because my_dsa is special!"
308 "\n(likewise for self.dsa_by_guid)" %
311 self.dsa_by_dnstr[self.my_dsa_dnstr] = self.my_dsa
312 self.dsa_by_guid[str(self.my_dsa.dsa_guid)] = self.my_dsa
314 def load_all_partitions(self):
315 """Discover and load all partitions.
317 Each NC is inserted into the part_table by partition
318 dn string (not the nCName dn string)
321 :raise: KCCError if partitions can't be found
324 res = self.samdb.search("CN=Partitions,%s" %
325 self.samdb.get_config_basedn(),
326 scope=ldb.SCOPE_SUBTREE,
327 expression="(objectClass=crossRef)")
328 except ldb.LdbError, (enum, estr):
329 raise KCCError("Unable to find partitions - (%s)" % estr)
332 partstr = str(msg.dn)
335 if partstr in self.part_table:
338 part = Partition(partstr)
340 part.load_partition(self.samdb)
341 self.part_table[partstr] = part
343 def refresh_failed_links_connections(self, ping=None):
344 """Ensure the failed links list is up to date
346 Based on MS-ADTS 6.2.2.1
348 :param ping: An oracle function of remote site availability
351 # LINKS: Refresh failed links
352 self.kcc_failed_links = {}
353 current, needed = self.my_dsa.get_rep_tables()
354 for replica in current.values():
355 # For every possible connection to replicate
356 for reps_from in replica.rep_repsFrom:
357 failure_count = reps_from.consecutive_sync_failures
358 if failure_count <= 0:
361 dsa_guid = str(reps_from.source_dsa_obj_guid)
362 time_first_failure = reps_from.last_success
363 last_result = reps_from.last_attempt
364 dns_name = reps_from.dns_name1
366 f = self.kcc_failed_links.get(dsa_guid)
368 f = KCCFailedObject(dsa_guid, failure_count,
369 time_first_failure, last_result,
371 self.kcc_failed_links[dsa_guid] = f
373 f.failure_count = max(f.failure_count, failure_count)
374 f.time_first_failure = min(f.time_first_failure,
376 f.last_result = last_result
378 # CONNECTIONS: Refresh failed connections
379 restore_connections = set()
381 DEBUG("refresh_failed_links: checking if links are still down")
382 for connection in self.kcc_failed_connections:
383 if ping(connection.dns_name):
384 # Failed connection is no longer failing
385 restore_connections.add(connection)
387 connection.failure_count += 1
389 DEBUG("refresh_failed_links: not checking live links because we\n"
390 "weren't asked to --attempt-live-connections")
392 # Remove the restored connections from the failed connections
393 self.kcc_failed_connections.difference_update(restore_connections)
395 def is_stale_link_connection(self, target_dsa):
396 """Check whether a link to a remote DSA is stale
398 Used in MS-ADTS 6.2.2.2 Intrasite Connection Creation
400 Returns True if the remote seems to have been down for at
401 least two hours, otherwise False.
403 :param target_dsa: the remote DSA object
404 :return: True if link is stale, otherwise False
406 failed_link = self.kcc_failed_links.get(str(target_dsa.dsa_guid))
408 # failure_count should be > 0, but check anyways
409 if failed_link.failure_count > 0:
410 unix_first_failure = \
411 nttime2unix(failed_link.time_first_failure)
412 # TODO guard against future
413 if unix_first_failure > self.unix_now:
414 logger.error("The last success time attribute for \
415 repsFrom is in the future!")
417 # Perform calculation in seconds
418 if (self.unix_now - unix_first_failure) > 60 * 60 * 2:
422 # We have checked failed *links*, but we also need to check
427 # TODO: This should be backed by some form of local database
428 def remove_unneeded_failed_links_connections(self):
429 # Remove all tuples in kcc_failed_links where failure count = 0
430 # In this implementation, this should never happen.
432 # Remove all connections which were not used this run or connections
433 # that became active during this run.
436 def remove_unneeded_ntdsconn(self, all_connected):
437 """Remove unneeded NTDS Connections once topology is calculated
439 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections
441 :param all_connected: indicates whether all sites are connected
446 # New connections won't have GUIDs which are needed for
448 for cn_conn in mydsa.connect_table.values():
449 if cn_conn.guid is None:
451 cn_conn.guid = misc.GUID(str(uuid.uuid4()))
452 cn_conn.whenCreated = self.nt_now
454 cn_conn.load_connection(self.samdb)
456 for cn_conn in mydsa.connect_table.values():
458 s_dnstr = cn_conn.get_from_dnstr()
460 cn_conn.to_be_deleted = True
463 #XXX should an RODC be regarded as same site
464 same_site = s_dnstr in self.my_site.dsa_table
466 # Given an nTDSConnection object cn, if the DC with the
467 # nTDSDSA object dc that is the parent object of cn and
468 # the DC with the nTDSDA object referenced by cn!fromServer
469 # are in the same site, the KCC on dc deletes cn if all of
470 # the following are true:
472 # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
474 # No site settings object s exists for the local DC's site, or
475 # bit NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED is clear in
478 # Another nTDSConnection object cn2 exists such that cn and
479 # cn2 have the same parent object, cn!fromServer = cn2!fromServer,
482 # cn!whenCreated < cn2!whenCreated
484 # cn!whenCreated = cn2!whenCreated and
485 # cn!objectGUID < cn2!objectGUID
487 # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
489 if not cn_conn.is_generated():
492 if self.my_site.is_cleanup_ntdsconn_disabled():
495 # Loop thru connections looking for a duplicate that
496 # fulfills the previous criteria
498 packed_guid = ndr_pack(cn_conn.guid)
499 for cn2_conn in mydsa.connect_table.values():
500 if cn2_conn is cn_conn:
503 s2_dnstr = cn2_conn.get_from_dnstr()
505 # If the NTDS Connections has a different
506 # fromServer field then no match
507 if s2_dnstr != s_dnstr:
510 lesser = (cn_conn.whenCreated < cn2_conn.whenCreated or
511 (cn_conn.whenCreated == cn2_conn.whenCreated and
512 packed_guid < ndr_pack(cn2_conn.guid)))
517 if lesser and not cn_conn.is_rodc_topology():
518 cn_conn.to_be_deleted = True
520 # Given an nTDSConnection object cn, if the DC with the nTDSDSA
521 # object dc that is the parent object of cn and the DC with
522 # the nTDSDSA object referenced by cn!fromServer are in
523 # different sites, a KCC acting as an ISTG in dc's site
524 # deletes cn if all of the following are true:
526 # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
528 # cn!fromServer references an nTDSDSA object for a DC
529 # in a site other than the local DC's site.
531 # The keepConnections sequence returned by
532 # CreateIntersiteConnections() does not contain
533 # cn!objectGUID, or cn is "superseded by" (see below)
534 # another nTDSConnection cn2 and keepConnections
535 # contains cn2!objectGUID.
537 # The return value of CreateIntersiteConnections()
540 # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in
543 else: # different site
545 if not mydsa.is_istg():
548 if not cn_conn.is_generated():
552 # We are directly using this connection in intersite or
553 # we are using a connection which can supersede this one.
555 # MS-ADTS 6.2.2.4 - Removing Unnecessary Connections does not
556 # appear to be correct.
558 # 1. cn!fromServer and cn!parent appear inconsistent with
560 # 2. The repsFrom do not imply each other
562 if cn_conn in self.kept_connections: # and not_superceded:
565 # This is the result of create_intersite_connections
566 if not all_connected:
569 if not cn_conn.is_rodc_topology():
570 cn_conn.to_be_deleted = True
572 if mydsa.is_ro() or self.readonly:
573 for connect in mydsa.connect_table.values():
574 if connect.to_be_deleted:
575 DEBUG_FN("TO BE DELETED:\n%s" % connect)
576 if connect.to_be_added:
577 DEBUG_FN("TO BE ADDED:\n%s" % connect)
579 # Peform deletion from our tables but perform
580 # no database modification
581 mydsa.commit_connections(self.samdb, ro=True)
583 # Commit any modified connections
584 mydsa.commit_connections(self.samdb)
586 def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
587 """Update an repsFrom object if required.
589 Part of MS-ADTS 6.2.2.5.
591 Update t_repsFrom if necessary to satisfy requirements. Such
592 updates are typically required when the IDL_DRSGetNCChanges
593 server has moved from one site to another--for example, to
594 enable compression when the server is moved from the
595 client's site to another site.
597 The repsFrom.update_flags bit field may be modified
598 auto-magically if any changes are made here. See
599 kcc_utils.RepsFromTo for gory details.
602 :param n_rep: NC replica we need
603 :param t_repsFrom: repsFrom tuple to modify
604 :param s_rep: NC replica at source DSA
605 :param s_dsa: source DSA
606 :param cn_conn: Local DSA NTDSConnection child
610 s_dnstr = s_dsa.dsa_dnstr
611 same_site = s_dnstr in self.my_site.dsa_table
613 # if schedule doesn't match then update and modify
614 times = convert_schedule_to_repltimes(cn_conn.schedule)
615 if times != t_repsFrom.schedule:
616 t_repsFrom.schedule = times
618 # Bit DRS_PER_SYNC is set in replicaFlags if and only
619 # if nTDSConnection schedule has a value v that specifies
620 # scheduled replication is to be performed at least once
622 if cn_conn.is_schedule_minimum_once_per_week():
624 if ((t_repsFrom.replica_flags &
625 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0):
626 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
628 # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
629 # if the source DSA and the local DC's nTDSDSA object are
630 # in the same site or source dsa is the FSMO role owner
631 # of one or more FSMO roles in the NC replica.
632 if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
634 if ((t_repsFrom.replica_flags &
635 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0):
636 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
638 # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
639 # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
640 # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
641 # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
642 # t.replicaFlags if and only if s and the local DC's
643 # nTDSDSA object are in different sites.
644 if ((cn_conn.options &
645 dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0):
647 if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
650 # it LOOKS as if this next test is a bit silly: it
651 # checks the flag then sets it if it not set; the same
652 # effect could be achieved by unconditionally setting
653 # it. But in fact the repsFrom object has special
654 # magic attached to it, and altering replica_flags has
655 # side-effects. That is bad in my opinion, but there
657 if ((t_repsFrom.replica_flags &
658 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
659 t_repsFrom.replica_flags |= \
660 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
664 if ((t_repsFrom.replica_flags &
665 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
666 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
668 # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
669 # and only if s and the local DC's nTDSDSA object are
670 # not in the same site and the
671 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
672 # clear in cn!options
673 if (not same_site and
675 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
677 if ((t_repsFrom.replica_flags &
678 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0):
679 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
681 # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
682 # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
683 if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
685 if ((t_repsFrom.replica_flags &
686 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0):
687 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
689 # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
690 # set in t.replicaFlags if and only if cn!enabledConnection = false.
691 if not cn_conn.is_enabled():
693 if ((t_repsFrom.replica_flags &
694 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0):
695 t_repsFrom.replica_flags |= \
696 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
698 if ((t_repsFrom.replica_flags &
699 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0):
700 t_repsFrom.replica_flags |= \
701 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
703 # If s and the local DC's nTDSDSA object are in the same site,
704 # cn!transportType has no value, or the RDN of cn!transportType
707 # Bit DRS_MAIL_REP in t.replicaFlags is clear.
709 # t.uuidTransport = NULL GUID.
711 # t.uuidDsa = The GUID-based DNS name of s.
715 # Bit DRS_MAIL_REP in t.replicaFlags is set.
717 # If x is the object with dsname cn!transportType,
718 # t.uuidTransport = x!objectGUID.
720 # Let a be the attribute identified by
721 # x!transportAddressAttribute. If a is
722 # the dNSHostName attribute, t.uuidDsa = the GUID-based
723 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
725 # It appears that the first statement i.e.
727 # "If s and the local DC's nTDSDSA object are in the same
728 # site, cn!transportType has no value, or the RDN of
729 # cn!transportType is CN=IP:"
731 # could be a slightly tighter statement if it had an "or"
732 # between each condition. I believe this should
735 # IF (same-site) OR (no-value) OR (type-ip)
737 # because IP should be the primary transport mechanism
738 # (even in inter-site) and the absense of the transportType
739 # attribute should always imply IP no matter if its multi-site
741 # NOTE MS-TECH INCORRECT:
743 # All indications point to these statements above being
744 # incorrectly stated:
746 # t.uuidDsa = The GUID-based DNS name of s.
748 # Let a be the attribute identified by
749 # x!transportAddressAttribute. If a is
750 # the dNSHostName attribute, t.uuidDsa = the GUID-based
751 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
753 # because the uuidDSA is a GUID and not a GUID-base DNS
754 # name. Nor can uuidDsa hold (s!parent)!a if not
755 # dNSHostName. What should have been said is:
757 # t.naDsa = The GUID-based DNS name of s
759 # That would also be correct if transportAddressAttribute
760 # were "mailAddress" because (naDsa) can also correctly
761 # hold the SMTP ISM service address.
763 nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
765 if ((t_repsFrom.replica_flags &
766 drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0):
767 t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
769 t_repsFrom.transport_guid = misc.GUID()
771 # See (NOTE MS-TECH INCORRECT) above
773 # XXX it looks like these conditionals are pointless, because
774 # the state will end up as `t_repsFrom.dns_name1 == nastr` in
775 # either case, BUT the repsFrom thing is magic and assigning
776 # to it alters some flags. So we try not to update it unless
778 if t_repsFrom.dns_name1 != nastr:
779 t_repsFrom.dns_name1 = nastr
781 if t_repsFrom.version > 0x1 and t_repsFrom.dns_name2 != nastr:
782 t_repsFrom.dns_name2 = nastr
785 if t_repsFrom.is_modified():
786 DEBUG_FN("modify_repsFrom(): %s" % t_repsFrom)
788 def get_dsa_for_implied_replica(self, n_rep, cn_conn):
789 """If a connection imply a replica, find the relevant DSA
791 Given a NC replica and NTDS Connection, determine if the
792 connection implies a repsFrom tuple should be present from the
793 source DSA listed in the connection to the naming context. If
794 it should be, return the DSA; otherwise return None.
796 Based on part of MS-ADTS 6.2.2.5
798 :param n_rep: NC replica
799 :param cn_conn: NTDS Connection
800 :return: source DSA or None
802 #XXX different conditions for "implies" than MS-ADTS 6.2.2
804 # NTDS Connection must satisfy all the following criteria
805 # to imply a repsFrom tuple is needed:
807 # cn!enabledConnection = true.
808 # cn!options does not contain NTDSCONN_OPT_RODC_TOPOLOGY.
809 # cn!fromServer references an nTDSDSA object.
811 if not cn_conn.is_enabled() or cn_conn.is_rodc_topology():
814 s_dnstr = cn_conn.get_from_dnstr()
815 s_dsa = self.get_dsa(s_dnstr)
817 # No DSA matching this source DN string?
821 # To imply a repsFrom tuple is needed, each of these
824 # An NC replica of the NC "is present" on the DC to
825 # which the nTDSDSA object referenced by cn!fromServer
828 # An NC replica of the NC "should be present" on
830 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
832 if s_rep is None or not s_rep.is_present():
835 # To imply a repsFrom tuple is needed, each of these
838 # The NC replica on the DC referenced by cn!fromServer is
839 # a writable replica or the NC replica that "should be
840 # present" on the local DC is a partial replica.
842 # The NC is not a domain NC, the NC replica that
843 # "should be present" on the local DC is a partial
844 # replica, cn!transportType has no value, or
845 # cn!transportType has an RDN of CN=IP.
847 implied = (not s_rep.is_ro() or n_rep.is_partial()) and \
848 (not n_rep.is_domain() or
849 n_rep.is_partial() or
850 cn_conn.transport_dnstr is None or
851 cn_conn.transport_dnstr.find("CN=IP") == 0)
857 def translate_ntdsconn(self, current_dsa=None):
858 """Adjust repsFrom to match NTDSConnections
860 This function adjusts values of repsFrom abstract attributes of NC
861 replicas on the local DC to match those implied by
862 nTDSConnection objects.
864 Based on [MS-ADTS] 6.2.2.5
866 :param current_dsa: optional DSA on whose behalf we are acting.
871 if current_dsa is None:
872 current_dsa = self.my_dsa
874 if current_dsa.is_translate_ntdsconn_disabled():
875 DEBUG_FN("skipping translate_ntdsconn() "
876 "because disabling flag is set")
879 DEBUG_FN("translate_ntdsconn(): enter")
881 current_rep_table, needed_rep_table = current_dsa.get_rep_tables()
883 # Filled in with replicas we currently have that need deleting
886 # We're using the MS notation names here to allow
887 # correlation back to the published algorithm.
889 # n_rep - NC replica (n)
890 # t_repsFrom - tuple (t) in n!repsFrom
891 # s_dsa - Source DSA of the replica. Defined as nTDSDSA
892 # object (s) such that (s!objectGUID = t.uuidDsa)
893 # In our IDL representation of repsFrom the (uuidDsa)
894 # attribute is called (source_dsa_obj_guid)
895 # cn_conn - (cn) is nTDSConnection object and child of the local
896 # DC's nTDSDSA object and (cn!fromServer = s)
897 # s_rep - source DSA replica of n
899 # If we have the replica and its not needed
900 # then we add it to the "to be deleted" list.
901 for dnstr in current_rep_table:
902 if dnstr not in needed_rep_table:
903 delete_reps.add(dnstr)
905 DEBUG_FN('current %d needed %d delete %d' % (len(current_rep_table),
906 len(needed_rep_table), len(delete_reps)))
909 DEBUG('deleting these reps: %s' % delete_reps)
910 for dnstr in delete_reps:
911 del current_rep_table[dnstr]
913 # Now perform the scan of replicas we'll need
914 # and compare any current repsFrom against the
916 for n_rep in needed_rep_table.values():
918 # load any repsFrom and fsmo roles as we'll
919 # need them during connection translation
920 n_rep.load_repsFrom(self.samdb)
921 n_rep.load_fsmo_roles(self.samdb)
923 # Loop thru the existing repsFrom tupples (if any)
924 # XXX This is a list and could contain duplicates
925 # (multiple load_repsFrom calls)
926 for t_repsFrom in n_rep.rep_repsFrom:
928 # for each tuple t in n!repsFrom, let s be the nTDSDSA
929 # object such that s!objectGUID = t.uuidDsa
930 guidstr = str(t_repsFrom.source_dsa_obj_guid)
931 s_dsa = self.get_dsa_by_guidstr(guidstr)
933 # Source dsa is gone from config (strange)
934 # so cleanup stale repsFrom for unlisted DSA
936 logger.warning("repsFrom source DSA guid (%s) not found" %
938 t_repsFrom.to_be_deleted = True
941 # Find the connection that this repsFrom would use. If
942 # there isn't a good one (i.e. non-RODC_TOPOLOGY,
943 # meaning non-FRS), we delete the repsFrom.
944 s_dnstr = s_dsa.dsa_dnstr
945 connections = current_dsa.get_connection_by_from_dnstr(s_dnstr)
946 for cn_conn in connections:
947 if not cn_conn.is_rodc_topology():
950 # no break means no non-rodc_topology connection exists
951 t_repsFrom.to_be_deleted = True
954 # KCC removes this repsFrom tuple if any of the following
956 # No NC replica of the NC "is present" on DSA that
957 # would be source of replica
959 # A writable replica of the NC "should be present" on
960 # the local DC, but a partial replica "is present" on
962 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
964 if s_rep is None or not s_rep.is_present() or \
965 (not n_rep.is_ro() and s_rep.is_partial()):
967 t_repsFrom.to_be_deleted = True
970 # If the KCC did not remove t from n!repsFrom, it updates t
971 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
973 # Loop thru connections and add implied repsFrom tuples
974 # for each NTDSConnection under our local DSA if the
975 # repsFrom is not already present
976 for cn_conn in current_dsa.connect_table.values():
978 s_dsa = self.get_dsa_for_implied_replica(n_rep, cn_conn)
982 # Loop thru the existing repsFrom tupples (if any) and
983 # if we already have a tuple for this connection then
984 # no need to proceed to add. It will have been changed
985 # to have the correct attributes above
986 for t_repsFrom in n_rep.rep_repsFrom:
987 guidstr = str(t_repsFrom.source_dsa_obj_guid)
989 if s_dsa is self.get_dsa_by_guidstr(guidstr):
996 # Create a new RepsFromTo and proceed to modify
997 # it according to specification
998 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
1000 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
1002 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
1004 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
1006 # Add to our NC repsFrom as this is newly computed
1007 if t_repsFrom.is_modified():
1008 n_rep.rep_repsFrom.append(t_repsFrom)
1011 # Display any to be deleted or modified repsFrom
1012 text = n_rep.dumpstr_to_be_deleted()
1014 logger.info("TO BE DELETED:\n%s" % text)
1015 text = n_rep.dumpstr_to_be_modified()
1017 logger.info("TO BE MODIFIED:\n%s" % text)
1019 # Peform deletion from our tables but perform
1020 # no database modification
1021 n_rep.commit_repsFrom(self.samdb, ro=True)
1023 # Commit any modified repsFrom to the NC replica
1024 n_rep.commit_repsFrom(self.samdb)
1026 def merge_failed_links(self, ping=None):
1027 """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
1029 The KCC on a writable DC attempts to merge the link and connection
1030 failure information from bridgehead DCs in its own site to help it
1031 identify failed bridgehead DCs.
1033 Based on MS-ADTS 6.2.2.3.2 "Merge of kCCFailedLinks and kCCFailedLinks
1036 :param ping: An oracle of current bridgehead availability
1039 # 1. Queries every bridgehead server in your site (other than yourself)
1040 # 2. For every ntDSConnection that references a server in a different
1041 # site merge all the failure info
1043 # XXX - not implemented yet
1044 if ping is not None:
1045 debug.DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1047 DEBUG_FN("skipping merge_failed_links() because it requires "
1048 "real network connections\n"
1049 "and we weren't asked to --attempt-live-connections")
1051 def setup_graph(self, part):
1052 """Set up an intersite graph
1054 An intersite graph has a Vertex for each site object, a
1055 MultiEdge for each SiteLink object, and a MutliEdgeSet for
1056 each siteLinkBridge object (or implied siteLinkBridge). It
1057 reflects the intersite topology in a slightly more abstract
1060 Roughly corresponds to MS-ADTS 6.2.2.3.4.3
1062 :param part: a Partition object
1063 :returns: an InterSiteGraph object
1065 # If 'Bridge all site links' is enabled and Win2k3 bridges required
1067 # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1068 # No documentation for this however, ntdsapi.h appears to have:
1069 # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1070 bridges_required = self.my_site.site_options & 0x00001002 == 0
1071 transport_guid = str(self.ip_transport.guid)
1073 g = setup_graph(part, self.site_table, transport_guid,
1074 self.sitelink_table, bridges_required)
1076 if self.verify or self.dot_file_dir is not None:
1078 for edge in g.edges:
1079 for a, b in itertools.combinations(edge.vertices, 2):
1080 dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1081 verify_properties = ()
1082 verify_and_dot('site_edges', dot_edges, directed=False,
1083 label=self.my_dsa_dnstr,
1084 properties=verify_properties, debug=DEBUG,
1086 dot_file_dir=self.dot_file_dir)
1090 def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1091 """Get a bridghead DC for a site.
1093 Part of MS-ADTS 6.2.2.3.4.4
1095 :param site: site object representing for which a bridgehead
1097 :param part: crossRef for NC to replicate.
1098 :param transport: interSiteTransport object for replication
1100 :param partial_ok: True if a DC containing a partial
1101 replica or a full replica will suffice, False if only
1102 a full replica will suffice.
1103 :param detect_failed: True to detect failed DCs and route
1104 replication traffic around them, False to assume no DC
1106 :return: dsa object for the bridgehead DC or None
1109 bhs = self.get_all_bridgeheads(site, part, transport,
1110 partial_ok, detect_failed)
1112 debug.DEBUG_MAGENTA("get_bridgehead:\n\tsitedn=%s\n\tbhdn=None" %
1116 debug.DEBUG_GREEN("get_bridgehead:\n\tsitedn=%s\n\tbhdn=%s" %
1117 (site.site_dnstr, bhs[0].dsa_dnstr))
1120 def get_all_bridgeheads(self, site, part, transport,
1121 partial_ok, detect_failed):
1122 """Get all bridghead DCs on a site satisfying the given criteria
1124 Part of MS-ADTS 6.2.2.3.4.4
1126 :param site: site object representing the site for which
1127 bridgehead DCs are desired.
1128 :param part: partition for NC to replicate.
1129 :param transport: interSiteTransport object for
1130 replication traffic.
1131 :param partial_ok: True if a DC containing a partial
1132 replica or a full replica will suffice, False if
1133 only a full replica will suffice.
1134 :param detect_failed: True to detect failed DCs and route
1135 replication traffic around them, FALSE to assume
1137 :return: list of dsa object for available bridgehead DCs
1141 if transport.name != "IP":
1142 raise KCCError("get_all_bridgeheads has run into a "
1143 "non-IP transport! %r"
1144 % (transport.name,))
1146 DEBUG_FN("get_all_bridgeheads")
1147 DEBUG_FN(site.rw_dsa_table)
1148 for dsa in site.rw_dsa_table.values():
1150 pdnstr = dsa.get_parent_dnstr()
1152 # IF t!bridgeheadServerListBL has one or more values and
1153 # t!bridgeheadServerListBL does not contain a reference
1154 # to the parent object of dc then skip dc
1155 if ((len(transport.bridgehead_list) != 0 and
1156 pdnstr not in transport.bridgehead_list)):
1159 # IF dc is in the same site as the local DC
1160 # IF a replica of cr!nCName is not in the set of NC replicas
1161 # that "should be present" on dc or a partial replica of the
1162 # NC "should be present" but partialReplicasOkay = FALSE
1164 if self.my_site.same_site(dsa):
1165 needed, ro, partial = part.should_be_present(dsa)
1166 if not needed or (partial and not partial_ok):
1168 rep = dsa.get_current_replica(part.nc_dnstr)
1171 # IF an NC replica of cr!nCName is not in the set of NC
1172 # replicas that "are present" on dc or a partial replica of
1173 # the NC "is present" but partialReplicasOkay = FALSE
1176 rep = dsa.get_current_replica(part.nc_dnstr)
1177 if rep is None or (rep.is_partial() and not partial_ok):
1180 # IF AmIRODC() and cr!nCName corresponds to default NC then
1181 # Let dsaobj be the nTDSDSA object of the dc
1182 # IF dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1184 if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1185 if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1188 # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1190 if self.is_bridgehead_failed(dsa, detect_failed):
1191 DEBUG("bridgehead is failed")
1194 DEBUG_FN("get_all_bridgeheads: dsadn=%s" % dsa.dsa_dnstr)
1197 # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1199 # SORT bhs such that all GC servers precede DCs that are not GC
1200 # servers, and otherwise by ascending objectGUID
1202 # SORT bhs in a random order
1203 if site.is_random_bridgehead_disabled():
1204 bhs.sort(sort_dsa_by_gc_and_guid)
1207 debug.DEBUG_YELLOW(bhs)
1210 def is_bridgehead_failed(self, dsa, detect_failed):
1211 """Determine whether a given DC is known to be in a failed state
1213 :param dsa: the bridgehead to test
1214 :param detect_failed: True to really check, False to assume no failure
1215 :return: True if and only if the DC should be considered failed
1217 Here we DEPART from the pseudo code spec which appears to be
1218 wrong. It says, in full:
1220 /***** BridgeheadDCFailed *****/
1221 /* Determine whether a given DC is known to be in a failed state.
1222 * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1223 * IN: detectFailedDCs - TRUE if and only failed DC detection is
1225 * RETURNS: TRUE if and only if the DC should be considered to be in a
1228 BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1230 IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1231 the options attribute of the site settings object for the local
1234 ELSEIF a tuple z exists in the kCCFailedLinks or
1235 kCCFailedConnections variables such that z.UUIDDsa =
1236 objectGUID, z.FailureCount > 1, and the current time -
1237 z.TimeFirstFailure > 2 hours
1240 RETURN detectFailedDCs
1244 where you will see detectFailedDCs is not behaving as
1245 advertised -- it is acting as a default return code in the
1246 event that a failure is not detected, not a switch turning
1247 detection on or off. Elsewhere the documentation seems to
1248 concur with the comment rather than the code.
1250 if not detect_failed:
1253 # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1254 # When DETECT_STALE_DISABLED, we can never know of if
1255 # it's in a failed state
1256 if self.my_site.site_options & 0x00000008:
1259 return self.is_stale_link_connection(dsa)
1261 def create_connection(self, part, rbh, rsite, transport,
1262 lbh, lsite, link_opt, link_sched,
1263 partial_ok, detect_failed):
1264 """Create an nTDSConnection object as specified if it doesn't exist.
1266 Part of MS-ADTS 6.2.2.3.4.5
1268 :param part: crossRef object for the NC to replicate.
1269 :param rbh: nTDSDSA object for DC to act as the
1270 IDL_DRSGetNCChanges server (which is in a site other
1271 than the local DC's site).
1272 :param rsite: site of the rbh
1273 :param transport: interSiteTransport object for the transport
1274 to use for replication traffic.
1275 :param lbh: nTDSDSA object for DC to act as the
1276 IDL_DRSGetNCChanges client (which is in the local DC's site).
1277 :param lsite: site of the lbh
1278 :param link_opt: Replication parameters (aggregated siteLink options,
1280 :param link_sched: Schedule specifying the times at which
1281 to begin replicating.
1282 :partial_ok: True if bridgehead DCs containing partial
1283 replicas of the NC are acceptable.
1284 :param detect_failed: True to detect failed DCs and route
1285 replication traffic around them, FALSE to assume no DC
1288 rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1290 rbh_table = {x.dsa_dnstr: x for x in rbhs_all}
1292 debug.DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all),
1293 [x.dsa_dnstr for x in rbhs_all]))
1295 # MS-TECH says to compute rbhs_avail but then doesn't use it
1296 # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1297 # partial_ok, detect_failed)
1299 lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1302 lbhs_all.append(lbh)
1304 debug.DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all),
1305 [x.dsa_dnstr for x in lbhs_all]))
1307 # MS-TECH says to compute lbhs_avail but then doesn't use it
1308 # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1309 # partial_ok, detect_failed)
1311 # FOR each nTDSConnection object cn such that the parent of cn is
1312 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1313 for ldsa in lbhs_all:
1314 for cn in ldsa.connect_table.values():
1316 rdsa = rbh_table.get(cn.from_dnstr)
1320 debug.DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1321 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1322 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1323 # cn!transportType references t
1324 if ((cn.is_generated() and
1325 not cn.is_rodc_topology() and
1326 cn.transport_guid == transport.guid)):
1328 # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1329 # cn!options and cn!schedule != sch
1330 # Perform an originating update to set cn!schedule to
1332 if ((not cn.is_user_owned_schedule() and
1333 not cn.is_equivalent_schedule(link_sched))):
1334 cn.schedule = link_sched
1335 cn.set_modified(True)
1337 # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1338 # NTDSCONN_OPT_USE_NOTIFY are set in cn
1339 if cn.is_override_notify_default() and \
1342 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1344 # Perform an originating update to clear bits
1345 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1346 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1347 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1349 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1350 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1351 cn.set_modified(True)
1356 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1358 # Perform an originating update to set bits
1359 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1360 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1361 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1363 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1364 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1365 cn.set_modified(True)
1367 # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1368 if cn.is_twoway_sync():
1370 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1372 # Perform an originating update to clear bit
1373 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1374 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1375 cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1376 cn.set_modified(True)
1381 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1383 # Perform an originating update to set bit
1384 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1385 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1386 cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1387 cn.set_modified(True)
1389 # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1391 if cn.is_intersite_compression_disabled():
1393 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1395 # Perform an originating update to clear bit
1396 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1399 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0):
1401 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1402 cn.set_modified(True)
1406 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1408 # Perform an originating update to set bit
1409 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1412 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1414 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1415 cn.set_modified(True)
1417 # Display any modified connection
1419 if cn.to_be_modified:
1420 logger.info("TO BE MODIFIED:\n%s" % cn)
1422 ldsa.commit_connections(self.samdb, ro=True)
1424 ldsa.commit_connections(self.samdb)
1427 valid_connections = 0
1429 # FOR each nTDSConnection object cn such that cn!parent is
1430 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1431 for ldsa in lbhs_all:
1432 for cn in ldsa.connect_table.values():
1434 rdsa = rbh_table.get(cn.from_dnstr)
1438 debug.DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1440 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1441 # cn!transportType references t) and
1442 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1443 if (((not cn.is_generated() or
1444 cn.transport_guid == transport.guid) and
1445 not cn.is_rodc_topology())):
1447 # LET rguid be the objectGUID of the nTDSDSA object
1448 # referenced by cn!fromServer
1449 # LET lguid be (cn!parent)!objectGUID
1451 # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1452 # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1453 # Increment cValidConnections by 1
1454 if ((not self.is_bridgehead_failed(rdsa, detect_failed) and
1455 not self.is_bridgehead_failed(ldsa, detect_failed))):
1456 valid_connections += 1
1458 # IF keepConnections does not contain cn!objectGUID
1459 # APPEND cn!objectGUID to keepConnections
1460 self.kept_connections.add(cn)
1463 debug.DEBUG_RED("valid connections %d" % valid_connections)
1464 DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1465 # IF cValidConnections = 0
1466 if valid_connections == 0:
1468 # LET opt be NTDSCONN_OPT_IS_GENERATED
1469 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1471 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1472 # SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1473 # NTDSCONN_OPT_USE_NOTIFY in opt
1474 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1475 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1476 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1478 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1479 # SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1480 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1481 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1483 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1485 # SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1487 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1488 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1490 # Perform an originating update to create a new nTDSConnection
1491 # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1492 # cn!options = opt, cn!transportType is a reference to t,
1493 # cn!fromServer is a reference to rbh, and cn!schedule = sch
1494 DEBUG_FN("new connection, KCC dsa: %s" % self.my_dsa.dsa_dnstr)
1495 cn = lbh.new_connection(opt, 0, transport,
1496 rbh.dsa_dnstr, link_sched)
1498 # Display any added connection
1501 logger.info("TO BE ADDED:\n%s" % cn)
1503 lbh.commit_connections(self.samdb, ro=True)
1505 lbh.commit_connections(self.samdb)
1507 # APPEND cn!objectGUID to keepConnections
1508 self.kept_connections.add(cn)
1510 def add_transports(self, vertex, local_vertex, graph, detect_failed):
1511 """Build a Vertex's transport lists
1513 Each vertex has accept_red_red and accept_black lists that
1514 list what transports they accept under various conditions. The
1515 only transport that is ever accepted is IP, and a dummy extra
1516 transport called "EDGE_TYPE_ALL".
1518 Part of MS-ADTS 6.2.2.3.4.3 -- ColorVertices
1520 :param vertex: the remote vertex we are thinking about
1521 :param local_vertex: the vertex relating to the local site.
1522 :param graph: the intersite graph
1523 :param detect_failed: whether to detect failed links
1524 :return: True if some bridgeheads were not found
1526 # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1527 # here, but using vertex seems to make more sense. That is,
1528 # the docs want this:
1530 #bh = self.get_bridgehead(vertex.site, vertex.part, transport,
1531 # local_vertex.is_black(), detect_failed)
1535 vertex.accept_red_red = []
1536 vertex.accept_black = []
1537 found_failed = False
1539 if vertex in graph.connected_vertices:
1540 t_guid = str(self.ip_transport.guid)
1542 bh = self.get_bridgehead(vertex.site, vertex.part,
1544 vertex.is_black(), detect_failed)
1546 if vertex.site.is_rodc_site():
1547 vertex.accept_red_red.append(t_guid)
1551 vertex.accept_red_red.append(t_guid)
1552 vertex.accept_black.append(t_guid)
1554 # Add additional transport to ensure another run of Dijkstra
1555 vertex.accept_red_red.append("EDGE_TYPE_ALL")
1556 vertex.accept_black.append("EDGE_TYPE_ALL")
1560 def create_connections(self, graph, part, detect_failed):
1561 """Create intersite NTDSConnections as needed by a partition
1563 Construct an NC replica graph for the NC identified by
1564 the given crossRef, then create any additional nTDSConnection
1567 :param graph: site graph.
1568 :param part: crossRef object for NC.
1569 :param detect_failed: True to detect failed DCs and route
1570 replication traffic around them, False to assume no DC
1573 Modifies self.kept_connections by adding any connections
1574 deemed to be "in use".
1576 :return: (all_connected, found_failed_dc)
1577 (all_connected) True if the resulting NC replica graph
1578 connects all sites that need to be connected.
1579 (found_failed_dc) True if one or more failed DCs were
1582 all_connected = True
1583 found_failed = False
1585 DEBUG_FN("create_connections(): enter\n"
1586 "\tpartdn=%s\n\tdetect_failed=%s" %
1587 (part.nc_dnstr, detect_failed))
1589 # XXX - This is a highly abbreviated function from the MS-TECH
1590 # ref. It creates connections between bridgeheads to all
1591 # sites that have appropriate replicas. Thus we are not
1592 # creating a minimum cost spanning tree but instead
1593 # producing a fully connected tree. This should produce
1594 # a full (albeit not optimal cost) replication topology.
1596 my_vertex = Vertex(self.my_site, part)
1597 my_vertex.color_vertex()
1599 for v in graph.vertices:
1601 if self.add_transports(v, my_vertex, graph, False):
1604 # No NC replicas for this NC in the site of the local DC,
1605 # so no nTDSConnection objects need be created
1606 if my_vertex.is_white():
1607 return all_connected, found_failed
1609 edge_list, n_components = get_spanning_tree_edges(graph,
1613 DEBUG_FN("%s Number of components: %d" %
1614 (part.nc_dnstr, n_components))
1615 if n_components > 1:
1616 all_connected = False
1618 # LET partialReplicaOkay be TRUE if and only if
1619 # localSiteVertex.Color = COLOR.BLACK
1620 partial_ok = my_vertex.is_black()
1622 # Utilize the IP transport only for now
1623 transport = self.ip_transport
1625 DEBUG("edge_list %s" % edge_list)
1627 # XXX more accurate comparison?
1628 if e.directed and e.vertices[0].site is self.my_site:
1631 if e.vertices[0].site is self.my_site:
1632 rsite = e.vertices[1].site
1634 rsite = e.vertices[0].site
1636 # We don't make connections to our own site as that
1637 # is intrasite topology generator's job
1638 if rsite is self.my_site:
1639 DEBUG("rsite is my_site")
1642 # Determine bridgehead server in remote site
1643 rbh = self.get_bridgehead(rsite, part, transport,
1644 partial_ok, detect_failed)
1648 # RODC acts as an BH for itself
1650 # LET lbh be the nTDSDSA object of the local DC
1652 # LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1653 # cr, t, partialReplicaOkay, detectFailedDCs)
1654 if self.my_dsa.is_ro():
1655 lsite = self.my_site
1658 lsite = self.my_site
1659 lbh = self.get_bridgehead(lsite, part, transport,
1660 partial_ok, detect_failed)
1663 debug.DEBUG_RED("DISASTER! lbh is None")
1666 DEBUG_FN("lsite: %s\nrsite: %s" %(lsite, rsite))
1667 DEBUG_FN("vertices %s" %(e.vertices,))
1668 debug.DEBUG_BLUE("bridgeheads\n%s\n%s\n%s" % (lbh, rbh, "-" * 70))
1670 sitelink = e.site_link
1671 if sitelink is None:
1675 link_opt = sitelink.options
1676 link_sched = sitelink.schedule
1678 self.create_connection(part, rbh, rsite, transport,
1679 lbh, lsite, link_opt, link_sched,
1680 partial_ok, detect_failed)
1682 return all_connected, found_failed
1684 def create_intersite_connections(self):
1685 """Create NTDSConnections as necessary for all partitions.
1687 Computes an NC replica graph for each NC replica that "should be
1688 present" on the local DC or "is present" on any DC in the same site
1689 as the local DC. For each edge directed to an NC replica on such a
1690 DC from an NC replica on a DC in another site, the KCC creates an
1691 nTDSConnection object to imply that edge if one does not already
1694 Modifies self.kept_connections - A set of nTDSConnection
1695 objects for edges that are directed
1696 to the local DC's site in one or more NC replica graphs.
1698 :return: True if spanning trees were created for all NC replica
1699 graphs, otherwise False.
1701 all_connected = True
1702 self.kept_connections = set()
1704 # LET crossRefList be the set containing each object o of class
1705 # crossRef such that o is a child of the CN=Partitions child of the
1708 # FOR each crossRef object cr in crossRefList
1709 # IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1710 # is clear in cr!systemFlags, skip cr.
1711 # LET g be the GRAPH return of SetupGraph()
1713 for part in self.part_table.values():
1715 if not part.is_enabled():
1718 if part.is_foreign():
1721 graph = self.setup_graph(part)
1723 # Create nTDSConnection objects, routing replication traffic
1724 # around "failed" DCs.
1725 found_failed = False
1727 connected, found_failed = self.create_connections(graph,
1730 DEBUG("with detect_failed: connected %s Found failed %s" %
1731 (connected, found_failed))
1733 all_connected = False
1736 # One or more failed DCs preclude use of the ideal NC
1737 # replica graph. Add connections for the ideal graph.
1738 self.create_connections(graph, part, False)
1740 return all_connected
1742 def intersite(self, ping):
1743 """Generate the inter-site KCC replica graph and nTDSConnections
1745 As per MS-ADTS 6.2.2.3.
1747 If self.readonly is False, the connections are added to self.samdb.
1749 Produces self.kept_connections which is a set of NTDS
1750 Connections that should be kept during subsequent pruning
1753 After this has run, all sites should be connected in a minimum
1756 :param ping: An oracle function of remote site availability
1757 :return (True or False): (True) if the produced NC replica
1758 graph connects all sites that need to be connected
1763 mysite = self.my_site
1764 all_connected = True
1766 DEBUG_FN("intersite(): enter")
1768 # Determine who is the ISTG
1770 mysite.select_istg(self.samdb, mydsa, ro=True)
1772 mysite.select_istg(self.samdb, mydsa, ro=False)
1774 # Test whether local site has topology disabled
1775 if mysite.is_intersite_topology_disabled():
1776 DEBUG_FN("intersite(): exit disabled all_connected=%d" %
1778 return all_connected
1780 if not mydsa.is_istg():
1781 DEBUG_FN("intersite(): exit not istg all_connected=%d" %
1783 return all_connected
1785 self.merge_failed_links(ping)
1787 # For each NC with an NC replica that "should be present" on the
1788 # local DC or "is present" on any DC in the same site as the
1789 # local DC, the KCC constructs a site graph--a precursor to an NC
1790 # replica graph. The site connectivity for a site graph is defined
1791 # by objects of class interSiteTransport, siteLink, and
1792 # siteLinkBridge in the config NC.
1794 all_connected = self.create_intersite_connections()
1796 DEBUG_FN("intersite(): exit all_connected=%d" % all_connected)
1797 return all_connected
1799 def update_rodc_connection(self):
1800 """Updates the RODC NTFRS connection object.
1802 If the local DSA is not an RODC, this does nothing.
1804 if not self.my_dsa.is_ro():
1807 # Given an nTDSConnection object cn1, such that cn1.options contains
1808 # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1809 # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1810 # that the following is true:
1812 # cn1.fromServer = cn2.fromServer
1813 # cn1.schedule = cn2.schedule
1815 # If no such cn2 can be found, cn1 is not modified.
1816 # If no such cn1 can be found, nothing is modified by this task.
1818 all_connections = self.my_dsa.connect_table.values()
1819 ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1820 rw_connections = [x for x in all_connections
1821 if x not in ro_connections]
1823 # XXX here we are dealing with multiple RODC_TOPO connections,
1824 # if they exist. It is not clear whether the spec means that
1825 # or if it ever arises.
1826 if rw_connections and ro_connections:
1827 for con in ro_connections:
1828 cn2 = rw_connections[0]
1829 con.from_dnstr = cn2.from_dnstr
1830 con.schedule = cn2.schedule
1831 con.to_be_modified = True
1833 self.my_dsa.commit_connections(self.samdb, ro=self.readonly)
1835 def intrasite_max_node_edges(self, node_count):
1836 """Find the maximum number of edges directed to an intrasite node
1838 The KCC does not create more than 50 edges directed to a
1839 single DC. To optimize replication, we compute that each node
1840 should have n+2 total edges directed to it such that (n) is
1841 the smallest non-negative integer satisfying
1842 (node_count <= 2*(n*n) + 6*n + 7)
1844 (If the number of edges is m (i.e. n + 2), that is the same as
1845 2 * m*m - 2 * m + 3). We think in terms of n because that is
1846 the number of extra connections over the double directed ring
1847 that exists by default.
1857 :param node_count: total number of nodes in the replica graph
1859 The intention is that there should be no more than 3 hops
1860 between any two DSAs at a site. With up to 7 nodes the 2 edges
1861 of the ring are enough; any configuration of extra edges with
1862 8 nodes will be enough. It is less clear that the 3 hop
1863 guarantee holds at e.g. 15 nodes in degenerate cases, but
1864 those are quite unlikely given the extra edges are randomly
1867 :param node_count: the number of nodes in the site
1868 "return: The desired maximum number of connections
1872 if node_count <= (2 * (n * n) + (6 * n) + 7):
1880 def construct_intrasite_graph(self, site_local, dc_local,
1881 nc_x, gc_only, detect_stale):
1882 """Create an intrasite graph using given parameters
1884 This might be called a number of times per site with different
1887 Based on [MS-ADTS] 6.2.2.2
1889 :param site_local: site for which we are working
1890 :param dc_local: local DC that potentially needs a replica
1891 :param nc_x: naming context (x) that we are testing if it
1892 "should be present" on the local DC
1893 :param gc_only: Boolean - only consider global catalog servers
1894 :param detect_stale: Boolean - check whether links seems down
1897 # We're using the MS notation names here to allow
1898 # correlation back to the published algorithm.
1900 # nc_x - naming context (x) that we are testing if it
1901 # "should be present" on the local DC
1902 # f_of_x - replica (f) found on a DC (s) for NC (x)
1903 # dc_s - DC where f_of_x replica was found
1904 # dc_local - local DC that potentially needs a replica
1906 # r_list - replica list R
1907 # p_of_x - replica (p) is partial and found on a DC (s)
1909 # l_of_x - replica (l) is the local replica for NC (x)
1910 # that should appear on the local DC
1911 # r_len = is length of replica list |R|
1913 # If the DSA doesn't need a replica for this
1914 # partition (NC x) then continue
1915 needed, ro, partial = nc_x.should_be_present(dc_local)
1917 debug.DEBUG_YELLOW("construct_intrasite_graph(): enter" +
1918 "\n\tgc_only=%d" % gc_only +
1919 "\n\tdetect_stale=%d" % detect_stale +
1920 "\n\tneeded=%s" % needed +
1922 "\n\tpartial=%s" % partial +
1926 debug.DEBUG_RED("%s lacks 'should be present' status, "
1927 "aborting construct_intersite_graph!" %
1931 # Create a NCReplica that matches what the local replica
1932 # should say. We'll use this below in our r_list
1933 l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid,
1936 l_of_x.identify_by_basedn(self.samdb)
1938 l_of_x.rep_partial = partial
1941 # Add this replica that "should be present" to the
1942 # needed replica table for this DSA
1943 dc_local.add_needed_replica(l_of_x)
1947 # Let R be a sequence containing each writable replica f of x
1948 # such that f "is present" on a DC s satisfying the following
1951 # * s is a writable DC other than the local DC.
1953 # * s is in the same site as the local DC.
1955 # * If x is a read-only full replica and x is a domain NC,
1956 # then the DC's functional level is at least
1957 # DS_BEHAVIOR_WIN2008.
1959 # * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
1960 # in the options attribute of the site settings object for
1961 # the local DC's site, or no tuple z exists in the
1962 # kCCFailedLinks or kCCFailedConnections variables such
1963 # that z.UUIDDsa is the objectGUID of the nTDSDSA object
1964 # for s, z.FailureCount > 0, and the current time -
1965 # z.TimeFirstFailure > 2 hours.
1969 # We'll loop thru all the DSAs looking for
1970 # writeable NC replicas that match the naming
1971 # context dn for (nc_x)
1973 for dc_s in self.my_site.dsa_table.values():
1974 # If this partition (nc_x) doesn't appear as a
1975 # replica (f_of_x) on (dc_s) then continue
1976 if not nc_x.nc_dnstr in dc_s.current_rep_table:
1979 # Pull out the NCReplica (f) of (x) with the dn
1980 # that matches NC (x) we are examining.
1981 f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
1983 # Replica (f) of NC (x) must be writable
1987 # Replica (f) of NC (x) must satisfy the
1988 # "is present" criteria for DC (s) that
1990 if not f_of_x.is_present():
1993 # DC (s) must be a writable DSA other than
1994 # my local DC. In other words we'd only replicate
1995 # from other writable DC
1996 if dc_s.is_ro() or dc_s is dc_local:
1999 # Certain replica graphs are produced only
2000 # for global catalogs, so test against
2001 # method input parameter
2002 if gc_only and not dc_s.is_gc():
2005 # DC (s) must be in the same site as the local DC
2006 # as this is the intra-site algorithm. This is
2007 # handled by virtue of placing DSAs in per
2008 # site objects (see enclosing for() loop)
2010 # If NC (x) is intended to be read-only full replica
2011 # for a domain NC on the target DC then the source
2012 # DC should have functional level at minimum WIN2008
2014 # Effectively we're saying that in order to replicate
2015 # to a targeted RODC (which was introduced in Windows 2008)
2016 # then we have to replicate from a DC that is also minimally
2019 # You can also see this requirement in the MS special
2020 # considerations for RODC which state that to deploy
2021 # an RODC, at least one writable domain controller in
2022 # the domain must be running Windows Server 2008
2023 if ro and not partial and nc_x.nc_type == NCType.domain:
2024 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2027 # If we haven't been told to turn off stale connection
2028 # detection and this dsa has a stale connection then
2030 if detect_stale and self.is_stale_link_connection(dc_s):
2033 # Replica meets criteria. Add it to table indexed
2034 # by the GUID of the DC that it appears on
2035 r_list.append(f_of_x)
2037 # If a partial (not full) replica of NC (x) "should be present"
2038 # on the local DC, append to R each partial replica (p of x)
2039 # such that p "is present" on a DC satisfying the same
2040 # criteria defined above for full replica DCs.
2042 # XXX This loop and the previous one differ only in whether
2043 # the replica is partial or not. here we only accept partial
2044 # (because we're partial); before we only accepted full. Order
2045 # doen't matter (the list is sorted a few lines down) so these
2046 # loops could easily be merged. Or this could be a helper
2050 # Now we loop thru all the DSAs looking for
2051 # partial NC replicas that match the naming
2052 # context dn for (NC x)
2053 for dc_s in self.my_site.dsa_table.values():
2055 # If this partition NC (x) doesn't appear as a
2056 # replica (p) of NC (x) on the dsa DC (s) then
2058 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2061 # Pull out the NCReplica with the dn that
2062 # matches NC (x) we are examining.
2063 p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2065 # Replica (p) of NC (x) must be partial
2066 if not p_of_x.is_partial():
2069 # Replica (p) of NC (x) must satisfy the
2070 # "is present" criteria for DC (s) that
2072 if not p_of_x.is_present():
2075 # DC (s) must be a writable DSA other than
2076 # my DSA. In other words we'd only replicate
2077 # from other writable DSA
2078 if dc_s.is_ro() or dc_s is dc_local:
2081 # Certain replica graphs are produced only
2082 # for global catalogs, so test against
2083 # method input parameter
2084 if gc_only and not dc_s.is_gc():
2087 # If we haven't been told to turn off stale connection
2088 # detection and this dsa has a stale connection then
2090 if detect_stale and self.is_stale_link_connection(dc_s):
2093 # Replica meets criteria. Add it to table indexed
2094 # by the GUID of the DSA that it appears on
2095 r_list.append(p_of_x)
2097 # Append to R the NC replica that "should be present"
2099 r_list.append(l_of_x)
2101 r_list.sort(sort_replica_by_dsa_guid)
2104 max_node_edges = self.intrasite_max_node_edges(r_len)
2106 # Add a node for each r_list element to the replica graph
2109 node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2110 graph_list.append(node)
2112 # For each r(i) from (0 <= i < |R|-1)
2114 while i < (r_len-1):
2115 # Add an edge from r(i) to r(i+1) if r(i) is a full
2116 # replica or r(i+1) is a partial replica
2117 if not r_list[i].is_partial() or r_list[i+1].is_partial():
2118 graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
2120 # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2121 # replica or ri is a partial replica.
2122 if not r_list[i+1].is_partial() or r_list[i].is_partial():
2123 graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
2126 # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2127 # or r0 is a partial replica.
2128 if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
2129 graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
2131 # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2132 # r|R|-1 is a partial replica.
2133 if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
2134 graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
2136 DEBUG("r_list is length %s" % len(r_list))
2137 DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr))
2140 do_dot_files = self.dot_file_dir is not None and self.debug
2141 if self.verify or do_dot_files:
2143 dot_vertices = set()
2144 for v1 in graph_list:
2145 dot_vertices.add(v1.dsa_dnstr)
2146 for v2 in v1.edge_from:
2147 dot_edges.append((v2, v1.dsa_dnstr))
2148 dot_vertices.add(v2)
2150 verify_properties = ('connected', 'directed_double_ring_or_small')
2151 verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2152 label='%s__%s__%s' % (site_local.site_dnstr,
2153 nctype_lut[nc_x.nc_type],
2155 properties=verify_properties, debug=DEBUG,
2157 dot_file_dir=self.dot_file_dir,
2160 # For each existing nTDSConnection object implying an edge
2161 # from rj of R to ri such that j != i, an edge from rj to ri
2162 # is not already in the graph, and the total edges directed
2163 # to ri is less than n+2, the KCC adds that edge to the graph.
2164 for vertex in graph_list:
2165 dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2166 for connect in dsa.connect_table.values():
2167 remote = connect.from_dnstr
2168 if remote in self.my_site.dsa_table:
2169 vertex.add_edge_from(remote)
2171 DEBUG('reps are: %s' % ' '.join(x.rep_dsa_dnstr for x in r_list))
2172 DEBUG('dsas are: %s' % ' '.join(x.dsa_dnstr for x in graph_list))
2174 for tnode in graph_list:
2175 # To optimize replication latency in sites with many NC
2176 # replicas, the KCC adds new edges directed to ri to bring
2177 # the total edges to n+2, where the NC replica rk of R
2178 # from which the edge is directed is chosen at random such
2179 # that k != i and an edge from rk to ri is not already in
2182 # Note that the KCC tech ref does not give a number for
2183 # the definition of "sites with many NC replicas". At a
2184 # bare minimum to satisfy n+2 edges directed at a node we
2185 # have to have at least three replicas in |R| (i.e. if n
2186 # is zero then at least replicas from two other graph
2187 # nodes may direct edges to us).
2188 if r_len >= 3 and not tnode.has_sufficient_edges():
2189 candidates = [x for x in graph_list if
2191 x.dsa_dnstr not in tnode.edge_from)]
2193 debug.DEBUG_BLUE("looking for random link for %s. r_len %d, "
2194 "graph len %d candidates %d"
2195 % (tnode.dsa_dnstr, r_len, len(graph_list),
2198 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2200 while candidates and not tnode.has_sufficient_edges():
2201 other = random.choice(candidates)
2202 DEBUG("trying to add candidate %s" % other.dsa_dstr)
2203 if not tnode.add_edge_from(other):
2204 debug.DEBUG_RED("could not add %s" % other.dsa_dstr)
2205 candidates.remove(other)
2207 DEBUG_FN("not adding links to %s: nodes %s, links is %s/%s" %
2208 (tnode.dsa_dnstr, r_len, len(tnode.edge_from),
2211 # Print the graph node in debug mode
2212 DEBUG_FN("%s" % tnode)
2214 # For each edge directed to the local DC, ensure a nTDSConnection
2215 # points to us that satisfies the KCC criteria
2217 if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2218 tnode.add_connections_from_edges(dc_local)
2220 if self.verify or do_dot_files:
2222 dot_vertices = set()
2223 for v1 in graph_list:
2224 dot_vertices.add(v1.dsa_dnstr)
2225 for v2 in v1.edge_from:
2226 dot_edges.append((v2, v1.dsa_dnstr))
2227 dot_vertices.add(v2)
2229 verify_properties = ('connected', 'directed_double_ring_or_small')
2230 verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2231 label='%s__%s__%s' % (site_local.site_dnstr,
2232 nctype_lut[nc_x.nc_type],
2234 properties=verify_properties, debug=DEBUG,
2236 dot_file_dir=self.dot_file_dir,
2239 def intrasite(self):
2240 """Generate the intrasite KCC connections
2242 As per MS-ADTS 6.2.2.2.
2244 If self.readonly is False, the connections are added to self.samdb.
2246 After this call, all DCs in each site with more than 3 DCs
2247 should be connected in a bidirectional ring. If a site has 2
2248 DCs, they will bidirectionally connected. Sites with many DCs
2249 may have arbitrary extra connections.
2255 DEBUG_FN("intrasite(): enter")
2257 # Test whether local site has topology disabled
2258 mysite = self.my_site
2259 if mysite.is_intrasite_topology_disabled():
2262 detect_stale = (not mysite.is_detect_stale_disabled())
2263 for connect in mydsa.connect_table.values():
2264 if connect.to_be_added:
2265 debug.DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2267 # Loop thru all the partitions, with gc_only False
2268 for partdn, part in self.part_table.items():
2269 self.construct_intrasite_graph(mysite, mydsa, part, False,
2271 for connect in mydsa.connect_table.values():
2272 if connect.to_be_added:
2273 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2275 # If the DC is a GC server, the KCC constructs an additional NC
2276 # replica graph (and creates nTDSConnection objects) for the
2277 # config NC as above, except that only NC replicas that "are present"
2278 # on GC servers are added to R.
2279 for connect in mydsa.connect_table.values():
2280 if connect.to_be_added:
2281 debug.DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2283 # Do it again, with gc_only True
2284 for partdn, part in self.part_table.items():
2285 if part.is_config():
2286 self.construct_intrasite_graph(mysite, mydsa, part, True,
2289 # The DC repeats the NC replica graph computation and nTDSConnection
2290 # creation for each of the NC replica graphs, this time assuming
2291 # that no DC has failed. It does so by re-executing the steps as
2292 # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2293 # set in the options attribute of the site settings object for
2294 # the local DC's site. (ie. we set "detec_stale" flag to False)
2295 for connect in mydsa.connect_table.values():
2296 if connect.to_be_added:
2297 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2299 # Loop thru all the partitions.
2300 for partdn, part in self.part_table.items():
2301 self.construct_intrasite_graph(mysite, mydsa, part, False,
2302 False) # don't detect stale
2304 # If the DC is a GC server, the KCC constructs an additional NC
2305 # replica graph (and creates nTDSConnection objects) for the
2306 # config NC as above, except that only NC replicas that "are present"
2307 # on GC servers are added to R.
2308 for connect in mydsa.connect_table.values():
2309 if connect.to_be_added:
2310 debug.DEBUG_RED("TO BE ADDED:\n%s" % connect)
2312 for partdn, part in self.part_table.items():
2313 if part.is_config():
2314 self.construct_intrasite_graph(mysite, mydsa, part, True,
2315 False) # don't detect stale
2318 # Display any to be added or modified repsFrom
2319 for connect in mydsa.connect_table.values():
2320 if connect.to_be_deleted:
2321 logger.info("TO BE DELETED:\n%s" % connect)
2322 if connect.to_be_modified:
2323 logger.info("TO BE MODIFIED:\n%s" % connect)
2324 if connect.to_be_added:
2325 debug.DEBUG_GREEN("TO BE ADDED:\n%s" % connect)
2327 mydsa.commit_connections(self.samdb, ro=True)
2329 # Commit any newly created connections to the samdb
2330 mydsa.commit_connections(self.samdb)
2332 def list_dsas(self):
2333 """Compile a comprehensive list of DSA DNs
2335 These are all the DSAs on all the sites that KCC would be
2338 This method is not idempotent and may not work correctly in
2339 sequence with KCC.run().
2341 :return: a list of DSA DN strings.
2346 self.load_all_sites()
2347 self.load_all_partitions()
2348 self.load_ip_transport()
2349 self.load_all_sitelinks()
2351 for site in self.site_table.values():
2352 dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2353 for dsa in site.dsa_table.values()])
2356 def load_samdb(self, dburl, lp, creds):
2357 """Load the database using an url, loadparm, and credentials
2359 :param dburl: a database url.
2360 :param lp: a loadparm object.
2361 :param creds: a Credentials object.
2363 self.samdb = SamDB(url=dburl,
2364 session_info=system_session(),
2365 credentials=creds, lp=lp)
2367 def plot_all_connections(self, basename, verify_properties=()):
2368 """Helper function to plot and verify NTDSConnections
2370 :param basename: an identifying string to use in filenames and logs.
2371 :param verify_properties: properties to verify (default empty)
2373 verify = verify_properties and self.verify
2374 if not verify and self.dot_file_dir is None:
2382 for dsa in self.dsa_by_dnstr.values():
2383 dot_vertices.append(dsa.dsa_dnstr)
2385 vertex_colours.append('#cc0000')
2387 vertex_colours.append('#0000cc')
2388 for con in dsa.connect_table.values():
2389 if con.is_rodc_topology():
2390 edge_colours.append('red')
2392 edge_colours.append('blue')
2393 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2395 verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2396 label=self.my_dsa_dnstr, properties=verify_properties,
2397 debug=DEBUG, verify=verify, dot_file_dir=self.dot_file_dir,
2398 directed=True, edge_colors=edge_colours,
2399 vertex_colors=vertex_colours)
2401 def run(self, dburl, lp, creds, forced_local_dsa=None,
2402 forget_local_links=False, forget_intersite_links=False,
2403 attempt_live_connections=False):
2404 """Perform a KCC run, possibly updating repsFrom topology
2406 :param dburl: url of the database to work with.
2407 :param lp: a loadparm object.
2408 :param creds: a Credentials object.
2409 :param forced_local_dsa: pretend to be on the DSA with this dn_str
2410 :param forget_local_links: calculate as if no connections existed
2411 (boolean, default False)
2412 :param forget_intersite_links: calculate with only intrasite connection
2413 (boolean, default False)
2414 :param attempt_live_connections: attempt to connect to remote DSAs to
2415 determine link availability (boolean, default False)
2416 :return: 1 on error, 0 otherwise
2418 # We may already have a samdb setup if we are
2419 # currently importing an ldif for a test run
2420 if self.samdb is None:
2422 self.load_samdb(dburl, lp, creds)
2423 except ldb.LdbError, (num, msg):
2424 logger.error("Unable to open sam database %s : %s" %
2428 if forced_local_dsa:
2429 self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" %
2437 self.load_all_sites()
2438 self.load_all_partitions()
2439 self.load_ip_transport()
2440 self.load_all_sitelinks()
2442 if self.verify or self.dot_file_dir is not None:
2444 for site in self.site_table.values():
2445 guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2447 in site.dsa_table.items())
2449 self.plot_all_connections('dsa_initial')
2452 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2453 for dnstr, c_rep in current_reps.items():
2454 DEBUG("c_rep %s" % c_rep)
2455 dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2457 verify_and_dot('dsa_repsFrom_initial', dot_edges,
2458 directed=True, label=self.my_dsa_dnstr,
2459 properties=(), debug=DEBUG, verify=self.verify,
2460 dot_file_dir=self.dot_file_dir)
2463 for site in self.site_table.values():
2464 for dsa in site.dsa_table.values():
2465 current_reps, needed_reps = dsa.get_rep_tables()
2466 for dn_str, rep in current_reps.items():
2467 for reps_from in rep.rep_repsFrom:
2468 DEBUG("rep %s" % rep)
2469 dsa_guid = str(reps_from.source_dsa_obj_guid)
2470 dsa_dn = guid_to_dnstr[dsa_guid]
2471 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2473 verify_and_dot('dsa_repsFrom_initial_all', dot_edges,
2474 directed=True, label=self.my_dsa_dnstr,
2475 properties=(), debug=DEBUG, verify=self.verify,
2476 dot_file_dir=self.dot_file_dir)
2479 for link in self.sitelink_table.values():
2480 for a, b in itertools.combinations(link.site_list, 2):
2481 dot_edges.append((str(a), str(b)))
2482 properties = ('connected',)
2483 verify_and_dot('dsa_sitelink_initial', dot_edges,
2485 label=self.my_dsa_dnstr, properties=properties,
2486 debug=DEBUG, verify=self.verify,
2487 dot_file_dir=self.dot_file_dir)
2489 if forget_local_links:
2490 for dsa in self.my_site.dsa_table.values():
2491 dsa.connect_table = {k: v for k, v in
2492 dsa.connect_table.items()
2493 if v.is_rodc_topology()}
2494 self.plot_all_connections('dsa_forgotten_local')
2496 if forget_intersite_links:
2497 for site in self.site_table.values():
2498 for dsa in site.dsa_table.values():
2499 dsa.connect_table = {k: v for k, v in
2500 dsa.connect_table.items()
2501 if site is self.my_site and
2502 v.is_rodc_topology()}
2504 self.plot_all_connections('dsa_forgotten_all')
2506 if attempt_live_connections:
2507 # Encapsulates lp and creds in a function that
2508 # attempts connections to remote DSAs.
2509 def ping(self, dnsname):
2511 drs_utils.drsuapi_connect(dnsname, self.lp, self.creds)
2512 except drs_utils.drsException:
2517 # These are the published steps (in order) for the
2518 # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2521 self.refresh_failed_links_connections(ping)
2527 all_connected = self.intersite(ping)
2530 self.remove_unneeded_ntdsconn(all_connected)
2533 self.translate_ntdsconn()
2536 self.remove_unneeded_failed_links_connections()
2539 self.update_rodc_connection()
2541 if self.verify or self.dot_file_dir is not None:
2542 self.plot_all_connections('dsa_final',
2545 debug.DEBUG_MAGENTA("there are %d dsa guids" %
2550 my_dnstr = self.my_dsa.dsa_dnstr
2551 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2552 for dnstr, n_rep in needed_reps.items():
2553 for reps_from in n_rep.rep_repsFrom:
2554 guid_str = str(reps_from.source_dsa_obj_guid)
2555 dot_edges.append((my_dnstr, guid_to_dnstr[guid_str]))
2556 edge_colors.append('#' + str(n_rep.nc_guid)[:6])
2558 verify_and_dot('dsa_repsFrom_final', dot_edges, directed=True,
2559 label=self.my_dsa_dnstr,
2560 properties=(), debug=DEBUG, verify=self.verify,
2561 dot_file_dir=self.dot_file_dir,
2562 edge_colors=edge_colors)
2566 for site in self.site_table.values():
2567 for dsa in site.dsa_table.values():
2568 current_reps, needed_reps = dsa.get_rep_tables()
2569 for n_rep in needed_reps.values():
2570 for reps_from in n_rep.rep_repsFrom:
2571 dsa_guid = str(reps_from.source_dsa_obj_guid)
2572 dsa_dn = guid_to_dnstr[dsa_guid]
2573 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2575 verify_and_dot('dsa_repsFrom_final_all', dot_edges,
2576 directed=True, label=self.my_dsa_dnstr,
2577 properties=(), debug=DEBUG, verify=self.verify,
2578 dot_file_dir=self.dot_file_dir)
2585 def import_ldif(self, dburl, lp, creds, ldif_file, forced_local_dsa=None):
2586 """Import relevant objects and attributes from an LDIF file.
2588 The point of this function is to allow a programmer/debugger to
2589 import an LDIF file with non-security relevent information that
2590 was previously extracted from a DC database. The LDIF file is used
2591 to create a temporary abbreviated database. The KCC algorithm can
2592 then run against this abbreviated database for debug or test
2593 verification that the topology generated is computationally the
2594 same between different OSes and algorithms.
2596 :param dburl: path to the temporary abbreviated db to create
2597 :param lp: a loadparm object.
2598 :param cred: a Credentials object.
2599 :param ldif_file: path to the ldif file to import
2600 :param forced_local_dsa: perform KCC from this DSA's point of view
2601 :return: zero on success, 1 on error
2604 self.samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif_file,
2606 except ldif_import_export.LdifError, e:
2607 print >> sys.stderr, e
2611 def export_ldif(self, dburl, lp, creds, ldif_file):
2612 """Save KCC relevant details to an ldif file
2614 The point of this function is to allow a programmer/debugger to
2615 extract an LDIF file with non-security relevent information from
2616 a DC database. The LDIF file can then be used to "import" via
2617 the import_ldif() function this file into a temporary abbreviated
2618 database. The KCC algorithm can then run against this abbreviated
2619 database for debug or test verification that the topology generated
2620 is computationally the same between different OSes and algorithms.
2622 :param dburl: LDAP database URL to extract info from
2623 :param lp: a loadparm object.
2624 :param cred: a Credentials object.
2625 :param ldif_file: output LDIF file name to create
2626 :return: zero on success, 1 on error
2629 ldif_import_export.samdb_to_ldif_file(self.samdb, dburl, lp, creds,
2631 except ldif_import_export.LdifError, e:
2632 print >> sys.stderr, e