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 readonly: 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.debug("Samba KCC is ignoring the obsolete "
172 logger.warning("Samba KCC does not support the transport "
173 "called %r." % (transport.name,))
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_query = "<GUID=%s>" % self.samdb.get_ntds_GUID()
266 dn = ldb.Dn(self.samdb, dn_query)
268 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
269 attrs=["objectGUID"])
270 except ldb.LdbError, (enum, estr):
271 DEBUG_FN("Search for dn '%s' [from %s] failed: %s. "
272 "This typically happens in --importldif mode due "
273 "to lack of module support." % (dn, dn_query, estr))
275 # We work around the failure above by looking at the
276 # dsServiceName that was put in the fake rootdse by
277 # the --exportldif, rather than the
278 # samdb.get_ntds_GUID(). The disadvantage is that this
279 # mode requires we modify the @ROOTDSE dnq to support
281 service_name_res = self.samdb.search(base="",
282 scope=ldb.SCOPE_BASE,
283 attrs=["dsServiceName"])
284 dn = ldb.Dn(self.samdb,
285 service_name_res[0]["dsServiceName"][0])
287 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
288 attrs=["objectGUID"])
289 except ldb.LdbError, (enum, estr):
290 raise KCCError("Unable to find my nTDSDSA - (%s)" % estr)
293 raise KCCError("Unable to find my nTDSDSA at %s" %
296 ntds_guid = misc.GUID(self.samdb.get_ntds_GUID())
297 if misc.GUID(res[0]["objectGUID"][0]) != ntds_guid:
298 raise KCCError("Did not find the GUID we expected,"
299 " perhaps due to --importldif")
301 self.my_dsa_dnstr = str(res[0].dn)
303 self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
305 if self.my_dsa_dnstr not in self.dsa_by_dnstr:
306 debug.DEBUG_DARK_YELLOW("my_dsa %s isn't in self.dsas_by_dnstr:"
307 " it must be RODC.\n"
308 "Let's add it, because my_dsa is special!"
309 "\n(likewise for self.dsa_by_guid)" %
312 self.dsa_by_dnstr[self.my_dsa_dnstr] = self.my_dsa
313 self.dsa_by_guid[str(self.my_dsa.dsa_guid)] = self.my_dsa
315 def load_all_partitions(self):
316 """Discover and load all partitions.
318 Each NC is inserted into the part_table by partition
319 dn string (not the nCName dn string)
322 :raise: KCCError if partitions can't be found
325 res = self.samdb.search("CN=Partitions,%s" %
326 self.samdb.get_config_basedn(),
327 scope=ldb.SCOPE_SUBTREE,
328 expression="(objectClass=crossRef)")
329 except ldb.LdbError, (enum, estr):
330 raise KCCError("Unable to find partitions - (%s)" % estr)
333 partstr = str(msg.dn)
336 if partstr in self.part_table:
339 part = Partition(partstr)
341 part.load_partition(self.samdb)
342 self.part_table[partstr] = part
344 def refresh_failed_links_connections(self, ping=None):
345 """Ensure the failed links list is up to date
347 Based on MS-ADTS 6.2.2.1
349 :param ping: An oracle function of remote site availability
352 # LINKS: Refresh failed links
353 self.kcc_failed_links = {}
354 current, needed = self.my_dsa.get_rep_tables()
355 for replica in current.values():
356 # For every possible connection to replicate
357 for reps_from in replica.rep_repsFrom:
358 failure_count = reps_from.consecutive_sync_failures
359 if failure_count <= 0:
362 dsa_guid = str(reps_from.source_dsa_obj_guid)
363 time_first_failure = reps_from.last_success
364 last_result = reps_from.last_attempt
365 dns_name = reps_from.dns_name1
367 f = self.kcc_failed_links.get(dsa_guid)
369 f = KCCFailedObject(dsa_guid, failure_count,
370 time_first_failure, last_result,
372 self.kcc_failed_links[dsa_guid] = f
374 f.failure_count = max(f.failure_count, failure_count)
375 f.time_first_failure = min(f.time_first_failure,
377 f.last_result = last_result
379 # CONNECTIONS: Refresh failed connections
380 restore_connections = set()
382 DEBUG("refresh_failed_links: checking if links are still down")
383 for connection in self.kcc_failed_connections:
384 if ping(connection.dns_name):
385 # Failed connection is no longer failing
386 restore_connections.add(connection)
388 connection.failure_count += 1
390 DEBUG("refresh_failed_links: not checking live links because we\n"
391 "weren't asked to --attempt-live-connections")
393 # Remove the restored connections from the failed connections
394 self.kcc_failed_connections.difference_update(restore_connections)
396 def is_stale_link_connection(self, target_dsa):
397 """Check whether a link to a remote DSA is stale
399 Used in MS-ADTS 6.2.2.2 Intrasite Connection Creation
401 Returns True if the remote seems to have been down for at
402 least two hours, otherwise False.
404 :param target_dsa: the remote DSA object
405 :return: True if link is stale, otherwise False
407 failed_link = self.kcc_failed_links.get(str(target_dsa.dsa_guid))
409 # failure_count should be > 0, but check anyways
410 if failed_link.failure_count > 0:
411 unix_first_failure = \
412 nttime2unix(failed_link.time_first_failure)
413 # TODO guard against future
414 if unix_first_failure > self.unix_now:
415 logger.error("The last success time attribute for \
416 repsFrom is in the future!")
418 # Perform calculation in seconds
419 if (self.unix_now - unix_first_failure) > 60 * 60 * 2:
423 # We have checked failed *links*, but we also need to check
428 # TODO: This should be backed by some form of local database
429 def remove_unneeded_failed_links_connections(self):
430 # Remove all tuples in kcc_failed_links where failure count = 0
431 # In this implementation, this should never happen.
433 # Remove all connections which were not used this run or connections
434 # that became active during this run.
437 def _ensure_connections_are_loaded(self, connections):
438 """Load or fake-load NTDSConnections lacking GUIDs
440 New connections don't have GUIDs and created times which are
441 needed for sorting. If we're in read-only mode, we make fake
442 GUIDs, otherwise we ask SamDB to do it for us.
444 :param connections: an iterable of NTDSConnection objects.
447 for cn_conn in connections:
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 def _mark_broken_ntdsconn(self):
456 """Find NTDS Connections that lack a remote
458 I'm not sure how they appear. Let's be rid of them by marking
459 them with the to_be_deleted attribute.
463 for cn_conn in self.my_dsa.connect_table.values():
464 s_dnstr = cn_conn.get_from_dnstr()
466 DEBUG_FN("%s has phantom connection %s" % (self.my_dsa,
468 cn_conn.to_be_deleted = True
470 def _mark_unneeded_local_ntdsconn(self):
471 """Find unneeded intrasite NTDS Connections for removal
473 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections.
474 Every DC removes its own unnecessary intrasite connections.
475 This function tags them with the to_be_deleted attribute.
479 # XXX should an RODC be regarded as same site? It isn't part
480 # of the intrasite ring.
482 if self.my_site.is_cleanup_ntdsconn_disabled():
483 DEBUG_FN("not doing ntdsconn cleanup for site %s, "
484 "because it is disabled" % self.my_site)
490 self._ensure_connections_are_loaded(mydsa.connect_table.values())
492 # RODC never actually added any connections to begin with
496 local_connections = []
498 for cn_conn in mydsa.connect_table.values():
499 s_dnstr = cn_conn.get_from_dnstr()
500 if s_dnstr in self.my_site.dsa_table:
501 removable = not (cn_conn.is_generated() or
502 cn_conn.is_rodc_topology())
503 packed_guid = ndr_pack(cn_conn.guid)
504 local_connections.append((cn_conn, s_dnstr,
505 packed_guid, removable))
507 for a, b in itertools.permutations(local_connections, 2):
508 cn_conn, s_dnstr, packed_guid, removable = a
509 cn_conn2, s_dnstr2, packed_guid2, removable2 = b
511 s_dnstr == s_dnstr2 and
512 cn_conn.whenCreated < cn_conn2.whenCreated or
513 (cn_conn.whenCreated == cn_conn2.whenCreated and
514 packed_guid < packed_guid2)):
515 cn_conn.to_be_deleted = True
517 def _mark_unneeded_intersite_ntdsconn(self):
518 """find unneeded intersite NTDS Connections for removal
520 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections. The
521 intersite topology generator removes links for all DCs in its
522 site. Here we just tag them with the to_be_deleted attribute.
526 # TODO Figure out how best to handle the RODC case
527 # The RODC is ISTG, but shouldn't act on anyone's behalf.
528 if self.my_dsa.is_ro():
531 # Find the intersite connections
532 local_dsas = self.my_site.dsa_table
533 connections_and_dsas = []
534 for dsa in local_dsas.values():
535 for cn in dsa.connect_table.values():
538 s_dnstr = cn.get_from_dnstr()
541 if s_dnstr not in local_dsas:
542 from_dsa = self.get_dsa(s_dnstr)
543 # Samba ONLY: ISTG removes connections to dead DCs
544 if from_dsa is None or '\\0ADEL' in s_dnstr:
545 logger.info("DSA appears deleted, removing connection %s"
547 cn.to_be_deleted = True
549 connections_and_dsas.append((cn, dsa, from_dsa))
551 self._ensure_connections_are_loaded(x[0] for x in connections_and_dsas)
552 for cn, to_dsa, from_dsa in connections_and_dsas:
553 if not cn.is_generated() or cn.is_rodc_topology():
556 # If the connection is in the kept_connections list, we
557 # only remove it if an endpoint seems down.
558 if (cn in self.kept_connections and
559 not (self.is_bridgehead_failed(to_dsa, True) or
560 self.is_bridgehead_failed(from_dsa, True))):
563 # this one is broken and might be superseded by another.
564 # But which other? Let's just say another link to the same
565 # site can supersede.
566 from_dnstr = from_dsa.dsa_dnstr
567 for site in self.site_table.values():
568 if from_dnstr in site.rw_dsa_table:
569 for cn2, to_dsa2, from_dsa2 in connections_and_dsas:
570 if (cn is not cn2 and
571 from_dsa2 in site.rw_dsa_table):
572 cn.to_be_deleted = True
574 def _commit_changes(self, dsa):
575 if dsa.is_ro() or self.readonly:
576 for connect in dsa.connect_table.values():
577 if connect.to_be_deleted:
578 logger.info("TO BE DELETED:\n%s" % connect)
579 if connect.to_be_added:
580 logger.info("TO BE ADDED:\n%s" % connect)
581 if connect.to_be_modified:
582 logger.info("TO BE MODIFIED:\n%s" % connect)
584 # Peform deletion from our tables but perform
585 # no database modification
586 dsa.commit_connections(self.samdb, ro=True)
588 # Commit any modified connections
589 dsa.commit_connections(self.samdb)
591 def remove_unneeded_ntdsconn(self, all_connected):
592 """Remove unneeded NTDS Connections once topology is calculated
594 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections
596 :param all_connected: indicates whether all sites are connected
599 self._mark_broken_ntdsconn()
600 self._mark_unneeded_local_ntdsconn()
601 # if we are not the istg, we're done!
602 # if we are the istg, but all_connected is False, we also do nothing.
603 if self.my_dsa.is_istg() and all_connected:
604 self._mark_unneeded_intersite_ntdsconn()
606 for dsa in self.my_site.dsa_table.values():
607 self._commit_changes(dsa)
609 def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
610 """Update an repsFrom object if required.
612 Part of MS-ADTS 6.2.2.5.
614 Update t_repsFrom if necessary to satisfy requirements. Such
615 updates are typically required when the IDL_DRSGetNCChanges
616 server has moved from one site to another--for example, to
617 enable compression when the server is moved from the
618 client's site to another site.
620 The repsFrom.update_flags bit field may be modified
621 auto-magically if any changes are made here. See
622 kcc_utils.RepsFromTo for gory details.
625 :param n_rep: NC replica we need
626 :param t_repsFrom: repsFrom tuple to modify
627 :param s_rep: NC replica at source DSA
628 :param s_dsa: source DSA
629 :param cn_conn: Local DSA NTDSConnection child
633 s_dnstr = s_dsa.dsa_dnstr
634 same_site = s_dnstr in self.my_site.dsa_table
636 # if schedule doesn't match then update and modify
637 times = convert_schedule_to_repltimes(cn_conn.schedule)
638 if times != t_repsFrom.schedule:
639 t_repsFrom.schedule = times
641 # Bit DRS_ADD_REF is set in replicaFlags unconditionally
643 if ((t_repsFrom.replica_flags &
644 drsuapi.DRSUAPI_DRS_ADD_REF) == 0x0):
645 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_ADD_REF
647 # Bit DRS_PER_SYNC is set in replicaFlags if and only
648 # if nTDSConnection schedule has a value v that specifies
649 # scheduled replication is to be performed at least once
651 if cn_conn.is_schedule_minimum_once_per_week():
653 if ((t_repsFrom.replica_flags &
654 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0):
655 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
657 # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
658 # if the source DSA and the local DC's nTDSDSA object are
659 # in the same site or source dsa is the FSMO role owner
660 # of one or more FSMO roles in the NC replica.
661 if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
663 if ((t_repsFrom.replica_flags &
664 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0):
665 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
667 # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
668 # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
669 # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
670 # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
671 # t.replicaFlags if and only if s and the local DC's
672 # nTDSDSA object are in different sites.
673 if ((cn_conn.options &
674 dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0):
676 if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
679 # it LOOKS as if this next test is a bit silly: it
680 # checks the flag then sets it if it not set; the same
681 # effect could be achieved by unconditionally setting
682 # it. But in fact the repsFrom object has special
683 # magic attached to it, and altering replica_flags has
684 # side-effects. That is bad in my opinion, but there
686 if ((t_repsFrom.replica_flags &
687 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
688 t_repsFrom.replica_flags |= \
689 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
693 if ((t_repsFrom.replica_flags &
694 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
695 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
697 # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
698 # and only if s and the local DC's nTDSDSA object are
699 # not in the same site and the
700 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
701 # clear in cn!options
702 if (not same_site and
704 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
706 if ((t_repsFrom.replica_flags &
707 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0):
708 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
710 # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
711 # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
712 if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
714 if ((t_repsFrom.replica_flags &
715 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0):
716 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
718 # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
719 # set in t.replicaFlags if and only if cn!enabledConnection = false.
720 if not cn_conn.is_enabled():
722 if ((t_repsFrom.replica_flags &
723 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0):
724 t_repsFrom.replica_flags |= \
725 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
727 if ((t_repsFrom.replica_flags &
728 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0):
729 t_repsFrom.replica_flags |= \
730 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
732 # If s and the local DC's nTDSDSA object are in the same site,
733 # cn!transportType has no value, or the RDN of cn!transportType
736 # Bit DRS_MAIL_REP in t.replicaFlags is clear.
738 # t.uuidTransport = NULL GUID.
740 # t.uuidDsa = The GUID-based DNS name of s.
744 # Bit DRS_MAIL_REP in t.replicaFlags is set.
746 # If x is the object with dsname cn!transportType,
747 # t.uuidTransport = x!objectGUID.
749 # Let a be the attribute identified by
750 # x!transportAddressAttribute. If a is
751 # the dNSHostName attribute, t.uuidDsa = the GUID-based
752 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
754 # It appears that the first statement i.e.
756 # "If s and the local DC's nTDSDSA object are in the same
757 # site, cn!transportType has no value, or the RDN of
758 # cn!transportType is CN=IP:"
760 # could be a slightly tighter statement if it had an "or"
761 # between each condition. I believe this should
764 # IF (same-site) OR (no-value) OR (type-ip)
766 # because IP should be the primary transport mechanism
767 # (even in inter-site) and the absense of the transportType
768 # attribute should always imply IP no matter if its multi-site
770 # NOTE MS-TECH INCORRECT:
772 # All indications point to these statements above being
773 # incorrectly stated:
775 # t.uuidDsa = The GUID-based DNS name of s.
777 # Let a be the attribute identified by
778 # x!transportAddressAttribute. If a is
779 # the dNSHostName attribute, t.uuidDsa = the GUID-based
780 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
782 # because the uuidDSA is a GUID and not a GUID-base DNS
783 # name. Nor can uuidDsa hold (s!parent)!a if not
784 # dNSHostName. What should have been said is:
786 # t.naDsa = The GUID-based DNS name of s
788 # That would also be correct if transportAddressAttribute
789 # were "mailAddress" because (naDsa) can also correctly
790 # hold the SMTP ISM service address.
792 nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
794 if ((t_repsFrom.replica_flags &
795 drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0):
796 t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
798 t_repsFrom.transport_guid = misc.GUID()
800 # See (NOTE MS-TECH INCORRECT) above
802 # NOTE: it looks like these conditionals are pointless,
803 # because the state will end up as `t_repsFrom.dns_name1 ==
804 # nastr` in either case, BUT the repsFrom thing is magic and
805 # assigning to it alters some flags. So we try not to update
806 # it unless necessary.
807 if t_repsFrom.dns_name1 != nastr:
808 t_repsFrom.dns_name1 = nastr
810 if t_repsFrom.version > 0x1 and t_repsFrom.dns_name2 != nastr:
811 t_repsFrom.dns_name2 = nastr
813 if t_repsFrom.is_modified():
814 DEBUG_FN("modify_repsFrom(): %s" % t_repsFrom)
816 def get_dsa_for_implied_replica(self, n_rep, cn_conn):
817 """If a connection imply a replica, find the relevant DSA
819 Given a NC replica and NTDS Connection, determine if the
820 connection implies a repsFrom tuple should be present from the
821 source DSA listed in the connection to the naming context. If
822 it should be, return the DSA; otherwise return None.
824 Based on part of MS-ADTS 6.2.2.5
826 :param n_rep: NC replica
827 :param cn_conn: NTDS Connection
828 :return: source DSA or None
830 # XXX different conditions for "implies" than MS-ADTS 6.2.2
833 # It boils down to: we want an enabled, non-FRS connections to
834 # a valid remote DSA with a non-RO replica corresponding to
837 if not cn_conn.is_enabled() or cn_conn.is_rodc_topology():
840 s_dnstr = cn_conn.get_from_dnstr()
841 s_dsa = self.get_dsa(s_dnstr)
843 # No DSA matching this source DN string?
847 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
849 if (s_rep is not None and
850 s_rep.is_present() and
851 (not s_rep.is_ro() or n_rep.is_partial())):
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.
870 if current_dsa is None:
871 current_dsa = self.my_dsa
873 if current_dsa.is_ro():
876 if current_dsa.is_translate_ntdsconn_disabled():
877 DEBUG_FN("skipping translate_ntdsconn() "
878 "because disabling flag is set")
881 DEBUG_FN("translate_ntdsconn(): enter")
883 current_rep_table, needed_rep_table = current_dsa.get_rep_tables()
885 # Filled in with replicas we currently have that need deleting
888 # We're using the MS notation names here to allow
889 # correlation back to the published algorithm.
891 # n_rep - NC replica (n)
892 # t_repsFrom - tuple (t) in n!repsFrom
893 # s_dsa - Source DSA of the replica. Defined as nTDSDSA
894 # object (s) such that (s!objectGUID = t.uuidDsa)
895 # In our IDL representation of repsFrom the (uuidDsa)
896 # attribute is called (source_dsa_obj_guid)
897 # cn_conn - (cn) is nTDSConnection object and child of the local
898 # DC's nTDSDSA object and (cn!fromServer = s)
899 # s_rep - source DSA replica of n
901 # If we have the replica and its not needed
902 # then we add it to the "to be deleted" list.
903 for dnstr in current_rep_table:
904 # If we're on the RODC, hardcode the update flags
906 c_rep = current_rep_table[dnstr]
907 c_rep.load_repsFrom(self.samdb)
908 for t_repsFrom in c_rep.rep_repsFrom:
909 replica_flags = (drsuapi.DRSUAPI_DRS_INIT_SYNC |
910 drsuapi.DRSUAPI_DRS_PER_SYNC |
911 drsuapi.DRSUAPI_DRS_ADD_REF |
912 drsuapi.DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING |
913 drsuapi.DRSUAPI_DRS_NONGC_RO_REP)
914 if t_repsFrom.replica_flags != replica_flags:
915 t_repsFrom.replica_flags = replica_flags
916 c_rep.commit_repsFrom(self.samdb, ro=self.readonly)
918 if dnstr not in needed_rep_table:
919 delete_reps.add(dnstr)
921 DEBUG_FN('current %d needed %d delete %d' % (len(current_rep_table),
922 len(needed_rep_table), len(delete_reps)))
925 # TODO Must delete repsFrom/repsTo for these replicas
926 DEBUG('deleting these reps: %s' % delete_reps)
927 for dnstr in delete_reps:
928 del current_rep_table[dnstr]
932 # Now perform the scan of replicas we'll need
933 # and compare any current repsFrom against the
935 for n_rep in needed_rep_table.values():
937 # load any repsFrom and fsmo roles as we'll
938 # need them during connection translation
939 n_rep.load_repsFrom(self.samdb)
940 n_rep.load_fsmo_roles(self.samdb)
942 # Loop thru the existing repsFrom tuples (if any)
943 # XXX This is a list and could contain duplicates
944 # (multiple load_repsFrom calls)
945 for t_repsFrom in n_rep.rep_repsFrom:
947 # for each tuple t in n!repsFrom, let s be the nTDSDSA
948 # object such that s!objectGUID = t.uuidDsa
949 guidstr = str(t_repsFrom.source_dsa_obj_guid)
950 s_dsa = self.get_dsa_by_guidstr(guidstr)
952 # Source dsa is gone from config (strange)
953 # so cleanup stale repsFrom for unlisted DSA
955 logger.warning("repsFrom source DSA guid (%s) not found" %
957 t_repsFrom.to_be_deleted = True
960 # Find the connection that this repsFrom would use. If
961 # there isn't a good one (i.e. non-RODC_TOPOLOGY,
962 # meaning non-FRS), we delete the repsFrom.
963 s_dnstr = s_dsa.dsa_dnstr
964 connections = current_dsa.get_connection_by_from_dnstr(s_dnstr)
965 for cn_conn in connections:
966 if not cn_conn.is_rodc_topology():
969 # no break means no non-rodc_topology connection exists
970 t_repsFrom.to_be_deleted = True
973 # KCC removes this repsFrom tuple if any of the following
975 # No NC replica of the NC "is present" on DSA that
976 # would be source of replica
978 # A writable replica of the NC "should be present" on
979 # the local DC, but a partial replica "is present" on
981 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
983 if s_rep is None or not s_rep.is_present() or \
984 (not n_rep.is_ro() and s_rep.is_partial()):
986 t_repsFrom.to_be_deleted = True
989 # If the KCC did not remove t from n!repsFrom, it updates t
990 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
992 # Loop thru connections and add implied repsFrom tuples
993 # for each NTDSConnection under our local DSA if the
994 # repsFrom is not already present
995 for cn_conn in current_dsa.connect_table.values():
997 s_dsa = self.get_dsa_for_implied_replica(n_rep, cn_conn)
1001 # Loop thru the existing repsFrom tuples (if any) and
1002 # if we already have a tuple for this connection then
1003 # no need to proceed to add. It will have been changed
1004 # to have the correct attributes above
1005 for t_repsFrom in n_rep.rep_repsFrom:
1006 guidstr = str(t_repsFrom.source_dsa_obj_guid)
1007 if s_dsa is self.get_dsa_by_guidstr(guidstr):
1014 # Create a new RepsFromTo and proceed to modify
1015 # it according to specification
1016 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
1018 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
1020 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
1022 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
1024 # Add to our NC repsFrom as this is newly computed
1025 if t_repsFrom.is_modified():
1026 n_rep.rep_repsFrom.append(t_repsFrom)
1028 if self.readonly or ro:
1029 # Display any to be deleted or modified repsFrom
1030 text = n_rep.dumpstr_to_be_deleted()
1032 logger.info("TO BE DELETED:\n%s" % text)
1033 text = n_rep.dumpstr_to_be_modified()
1035 logger.info("TO BE MODIFIED:\n%s" % text)
1037 # Peform deletion from our tables but perform
1038 # no database modification
1039 n_rep.commit_repsFrom(self.samdb, ro=True)
1041 # Commit any modified repsFrom to the NC replica
1042 n_rep.commit_repsFrom(self.samdb)
1046 # Now perform the scan of replicas we'll need
1047 # and compare any current repsTo against the
1050 # RODC should never push to anybody (should we check this?)
1054 for n_rep in needed_rep_table.values():
1056 # load any repsTo and fsmo roles as we'll
1057 # need them during connection translation
1058 n_rep.load_repsTo(self.samdb)
1060 # Loop thru the existing repsTo tuples (if any)
1061 # XXX This is a list and could contain duplicates
1062 # (multiple load_repsTo calls)
1063 for t_repsTo in n_rep.rep_repsTo:
1065 # for each tuple t in n!repsTo, let s be the nTDSDSA
1066 # object such that s!objectGUID = t.uuidDsa
1067 guidstr = str(t_repsTo.source_dsa_obj_guid)
1068 s_dsa = self.get_dsa_by_guidstr(guidstr)
1070 # Source dsa is gone from config (strange)
1071 # so cleanup stale repsTo for unlisted DSA
1073 logger.warning("repsTo source DSA guid (%s) not found" %
1075 t_repsTo.to_be_deleted = True
1078 # Find the connection that this repsTo would use. If
1079 # there isn't a good one (i.e. non-RODC_TOPOLOGY,
1080 # meaning non-FRS), we delete the repsTo.
1081 s_dnstr = s_dsa.dsa_dnstr
1082 if '\\0ADEL' in s_dnstr:
1083 logger.warning("repsTo source DSA guid (%s) appears deleted" %
1085 t_repsTo.to_be_deleted = True
1088 connections = s_dsa.get_connection_by_from_dnstr(self.my_dsa_dnstr)
1089 if len(connections) > 0:
1090 # Then this repsTo is tentatively valid
1093 # There is no plausible connection for this repsTo
1094 t_repsTo.to_be_deleted = True
1097 # Display any to be deleted or modified repsTo
1098 for rt in n_rep.rep_repsTo:
1099 if rt.to_be_deleted:
1100 logger.info("REMOVING REPS-TO: %s" % rt)
1102 # Peform deletion from our tables but perform
1103 # no database modification
1104 n_rep.commit_repsTo(self.samdb, ro=True)
1106 # Commit any modified repsTo to the NC replica
1107 n_rep.commit_repsTo(self.samdb)
1109 # TODO Remove any duplicate repsTo values. This should never happen in
1110 # any normal situations.
1112 def merge_failed_links(self, ping=None):
1113 """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
1115 The KCC on a writable DC attempts to merge the link and connection
1116 failure information from bridgehead DCs in its own site to help it
1117 identify failed bridgehead DCs.
1119 Based on MS-ADTS 6.2.2.3.2 "Merge of kCCFailedLinks and kCCFailedLinks
1122 :param ping: An oracle of current bridgehead availability
1125 # 1. Queries every bridgehead server in your site (other than yourself)
1126 # 2. For every ntDSConnection that references a server in a different
1127 # site merge all the failure info
1129 # XXX - not implemented yet
1130 if ping is not None:
1131 debug.DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1133 DEBUG_FN("skipping merge_failed_links() because it requires "
1134 "real network connections\n"
1135 "and we weren't asked to --attempt-live-connections")
1137 def setup_graph(self, part):
1138 """Set up an intersite graph
1140 An intersite graph has a Vertex for each site object, a
1141 MultiEdge for each SiteLink object, and a MutliEdgeSet for
1142 each siteLinkBridge object (or implied siteLinkBridge). It
1143 reflects the intersite topology in a slightly more abstract
1146 Roughly corresponds to MS-ADTS 6.2.2.3.4.3
1148 :param part: a Partition object
1149 :returns: an InterSiteGraph object
1151 # If 'Bridge all site links' is enabled and Win2k3 bridges required
1153 # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1154 # No documentation for this however, ntdsapi.h appears to have:
1155 # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1156 bridges_required = self.my_site.site_options & 0x00001002 != 0
1157 transport_guid = str(self.ip_transport.guid)
1159 g = setup_graph(part, self.site_table, transport_guid,
1160 self.sitelink_table, bridges_required)
1162 if self.verify or self.dot_file_dir is not None:
1164 for edge in g.edges:
1165 for a, b in itertools.combinations(edge.vertices, 2):
1166 dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1167 verify_properties = ()
1168 name = 'site_edges_%s' % part.partstr
1169 verify_and_dot(name, dot_edges, directed=False,
1170 label=self.my_dsa_dnstr,
1171 properties=verify_properties, debug=DEBUG,
1173 dot_file_dir=self.dot_file_dir)
1177 def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1178 """Get a bridghead DC for a site.
1180 Part of MS-ADTS 6.2.2.3.4.4
1182 :param site: site object representing for which a bridgehead
1184 :param part: crossRef for NC to replicate.
1185 :param transport: interSiteTransport object for replication
1187 :param partial_ok: True if a DC containing a partial
1188 replica or a full replica will suffice, False if only
1189 a full replica will suffice.
1190 :param detect_failed: True to detect failed DCs and route
1191 replication traffic around them, False to assume no DC
1193 :return: dsa object for the bridgehead DC or None
1196 bhs = self.get_all_bridgeheads(site, part, transport,
1197 partial_ok, detect_failed)
1199 debug.DEBUG_MAGENTA("get_bridgehead FAILED:\nsitedn = %s" %
1203 debug.DEBUG_GREEN("get_bridgehead:\n\tsitedn = %s\n\tbhdn = %s" %
1204 (site.site_dnstr, bhs[0].dsa_dnstr))
1207 def get_all_bridgeheads(self, site, part, transport,
1208 partial_ok, detect_failed):
1209 """Get all bridghead DCs on a site satisfying the given criteria
1211 Part of MS-ADTS 6.2.2.3.4.4
1213 :param site: site object representing the site for which
1214 bridgehead DCs are desired.
1215 :param part: partition for NC to replicate.
1216 :param transport: interSiteTransport object for
1217 replication traffic.
1218 :param partial_ok: True if a DC containing a partial
1219 replica or a full replica will suffice, False if
1220 only a full replica will suffice.
1221 :param detect_failed: True to detect failed DCs and route
1222 replication traffic around them, FALSE to assume
1224 :return: list of dsa object for available bridgehead DCs
1228 if transport.name != "IP":
1229 raise KCCError("get_all_bridgeheads has run into a "
1230 "non-IP transport! %r"
1231 % (transport.name,))
1233 DEBUG_FN(site.rw_dsa_table)
1234 for dsa in site.rw_dsa_table.values():
1236 pdnstr = dsa.get_parent_dnstr()
1238 # IF t!bridgeheadServerListBL has one or more values and
1239 # t!bridgeheadServerListBL does not contain a reference
1240 # to the parent object of dc then skip dc
1241 if ((len(transport.bridgehead_list) != 0 and
1242 pdnstr not in transport.bridgehead_list)):
1245 # IF dc is in the same site as the local DC
1246 # IF a replica of cr!nCName is not in the set of NC replicas
1247 # that "should be present" on dc or a partial replica of the
1248 # NC "should be present" but partialReplicasOkay = FALSE
1250 if self.my_site.same_site(dsa):
1251 needed, ro, partial = part.should_be_present(dsa)
1252 if not needed or (partial and not partial_ok):
1254 rep = dsa.get_current_replica(part.nc_dnstr)
1257 # IF an NC replica of cr!nCName is not in the set of NC
1258 # replicas that "are present" on dc or a partial replica of
1259 # the NC "is present" but partialReplicasOkay = FALSE
1262 rep = dsa.get_current_replica(part.nc_dnstr)
1263 if rep is None or (rep.is_partial() and not partial_ok):
1266 # IF AmIRODC() and cr!nCName corresponds to default NC then
1267 # Let dsaobj be the nTDSDSA object of the dc
1268 # IF dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1270 if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1271 if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1274 # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1276 if self.is_bridgehead_failed(dsa, detect_failed):
1277 DEBUG("bridgehead is failed")
1280 DEBUG_FN("found a bridgehead: %s" % dsa.dsa_dnstr)
1283 # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1285 # SORT bhs such that all GC servers precede DCs that are not GC
1286 # servers, and otherwise by ascending objectGUID
1288 # SORT bhs in a random order
1289 if site.is_random_bridgehead_disabled():
1290 bhs.sort(sort_dsa_by_gc_and_guid)
1293 debug.DEBUG_YELLOW(bhs)
1296 def is_bridgehead_failed(self, dsa, detect_failed):
1297 """Determine whether a given DC is known to be in a failed state
1299 :param dsa: the bridgehead to test
1300 :param detect_failed: True to really check, False to assume no failure
1301 :return: True if and only if the DC should be considered failed
1303 Here we DEPART from the pseudo code spec which appears to be
1304 wrong. It says, in full:
1306 /***** BridgeheadDCFailed *****/
1307 /* Determine whether a given DC is known to be in a failed state.
1308 * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1309 * IN: detectFailedDCs - TRUE if and only failed DC detection is
1311 * RETURNS: TRUE if and only if the DC should be considered to be in a
1314 BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1316 IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1317 the options attribute of the site settings object for the local
1320 ELSEIF a tuple z exists in the kCCFailedLinks or
1321 kCCFailedConnections variables such that z.UUIDDsa =
1322 objectGUID, z.FailureCount > 1, and the current time -
1323 z.TimeFirstFailure > 2 hours
1326 RETURN detectFailedDCs
1330 where you will see detectFailedDCs is not behaving as
1331 advertised -- it is acting as a default return code in the
1332 event that a failure is not detected, not a switch turning
1333 detection on or off. Elsewhere the documentation seems to
1334 concur with the comment rather than the code.
1336 if not detect_failed:
1339 # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1340 # When DETECT_STALE_DISABLED, we can never know of if
1341 # it's in a failed state
1342 if self.my_site.site_options & 0x00000008:
1345 return self.is_stale_link_connection(dsa)
1347 def create_connection(self, part, rbh, rsite, transport,
1348 lbh, lsite, link_opt, link_sched,
1349 partial_ok, detect_failed):
1350 """Create an nTDSConnection object as specified if it doesn't exist.
1352 Part of MS-ADTS 6.2.2.3.4.5
1354 :param part: crossRef object for the NC to replicate.
1355 :param rbh: nTDSDSA object for DC to act as the
1356 IDL_DRSGetNCChanges server (which is in a site other
1357 than the local DC's site).
1358 :param rsite: site of the rbh
1359 :param transport: interSiteTransport object for the transport
1360 to use for replication traffic.
1361 :param lbh: nTDSDSA object for DC to act as the
1362 IDL_DRSGetNCChanges client (which is in the local DC's site).
1363 :param lsite: site of the lbh
1364 :param link_opt: Replication parameters (aggregated siteLink options,
1366 :param link_sched: Schedule specifying the times at which
1367 to begin replicating.
1368 :partial_ok: True if bridgehead DCs containing partial
1369 replicas of the NC are acceptable.
1370 :param detect_failed: True to detect failed DCs and route
1371 replication traffic around them, FALSE to assume no DC
1374 rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1376 rbh_table = dict((x.dsa_dnstr, x) for x in rbhs_all)
1378 debug.DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all),
1379 [x.dsa_dnstr for x in rbhs_all]))
1381 # MS-TECH says to compute rbhs_avail but then doesn't use it
1382 # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1383 # partial_ok, detect_failed)
1385 lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1388 lbhs_all.append(lbh)
1390 debug.DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all),
1391 [x.dsa_dnstr for x in lbhs_all]))
1393 # MS-TECH says to compute lbhs_avail but then doesn't use it
1394 # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1395 # partial_ok, detect_failed)
1397 # FOR each nTDSConnection object cn such that the parent of cn is
1398 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1399 for ldsa in lbhs_all:
1400 for cn in ldsa.connect_table.values():
1402 rdsa = rbh_table.get(cn.from_dnstr)
1406 debug.DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1407 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1408 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1409 # cn!transportType references t
1410 if ((cn.is_generated() and
1411 not cn.is_rodc_topology() and
1412 cn.transport_guid == transport.guid)):
1414 # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1415 # cn!options and cn!schedule != sch
1416 # Perform an originating update to set cn!schedule to
1418 if ((not cn.is_user_owned_schedule() and
1419 not cn.is_equivalent_schedule(link_sched))):
1420 cn.schedule = link_sched
1421 cn.set_modified(True)
1423 # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1424 # NTDSCONN_OPT_USE_NOTIFY are set in cn
1425 if cn.is_override_notify_default() and \
1428 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1430 # Perform an originating update to clear bits
1431 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1432 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1433 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1435 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1436 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1437 cn.set_modified(True)
1442 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1444 # Perform an originating update to set bits
1445 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1446 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1447 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1449 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1450 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1451 cn.set_modified(True)
1453 # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1454 if cn.is_twoway_sync():
1456 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1458 # Perform an originating update to clear bit
1459 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1460 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1461 cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1462 cn.set_modified(True)
1467 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1469 # Perform an originating update to set bit
1470 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1471 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1472 cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1473 cn.set_modified(True)
1475 # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1477 if cn.is_intersite_compression_disabled():
1479 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1481 # Perform an originating update to clear bit
1482 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1485 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0):
1487 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1488 cn.set_modified(True)
1492 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1494 # Perform an originating update to set bit
1495 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1498 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1500 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1501 cn.set_modified(True)
1503 # Display any modified connection
1505 if cn.to_be_modified:
1506 logger.info("TO BE MODIFIED:\n%s" % cn)
1508 ldsa.commit_connections(self.samdb, ro=True)
1510 ldsa.commit_connections(self.samdb)
1513 valid_connections = 0
1515 # FOR each nTDSConnection object cn such that cn!parent is
1516 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1517 for ldsa in lbhs_all:
1518 for cn in ldsa.connect_table.values():
1520 rdsa = rbh_table.get(cn.from_dnstr)
1524 debug.DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1526 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1527 # cn!transportType references t) and
1528 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1529 if (((not cn.is_generated() or
1530 cn.transport_guid == transport.guid) and
1531 not cn.is_rodc_topology())):
1533 # LET rguid be the objectGUID of the nTDSDSA object
1534 # referenced by cn!fromServer
1535 # LET lguid be (cn!parent)!objectGUID
1537 # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1538 # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1539 # Increment cValidConnections by 1
1540 if ((not self.is_bridgehead_failed(rdsa, detect_failed) and
1541 not self.is_bridgehead_failed(ldsa, detect_failed))):
1542 valid_connections += 1
1544 # IF keepConnections does not contain cn!objectGUID
1545 # APPEND cn!objectGUID to keepConnections
1546 self.kept_connections.add(cn)
1549 debug.DEBUG_RED("valid connections %d" % valid_connections)
1550 DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1551 # IF cValidConnections = 0
1552 if valid_connections == 0:
1554 # LET opt be NTDSCONN_OPT_IS_GENERATED
1555 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1557 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1558 # SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1559 # NTDSCONN_OPT_USE_NOTIFY in opt
1560 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1561 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1562 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1564 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1565 # SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1566 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1567 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1569 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1571 # SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1573 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1574 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1576 # Perform an originating update to create a new nTDSConnection
1577 # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1578 # cn!options = opt, cn!transportType is a reference to t,
1579 # cn!fromServer is a reference to rbh, and cn!schedule = sch
1580 DEBUG_FN("new connection, KCC dsa: %s" % self.my_dsa.dsa_dnstr)
1581 system_flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1582 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1584 cn = lbh.new_connection(opt, system_flags, transport,
1585 rbh.dsa_dnstr, link_sched)
1587 # Display any added connection
1590 logger.info("TO BE ADDED:\n%s" % cn)
1592 lbh.commit_connections(self.samdb, ro=True)
1594 lbh.commit_connections(self.samdb)
1596 # APPEND cn!objectGUID to keepConnections
1597 self.kept_connections.add(cn)
1599 def add_transports(self, vertex, local_vertex, graph, detect_failed):
1600 """Build a Vertex's transport lists
1602 Each vertex has accept_red_red and accept_black lists that
1603 list what transports they accept under various conditions. The
1604 only transport that is ever accepted is IP, and a dummy extra
1605 transport called "EDGE_TYPE_ALL".
1607 Part of MS-ADTS 6.2.2.3.4.3 -- ColorVertices
1609 :param vertex: the remote vertex we are thinking about
1610 :param local_vertex: the vertex relating to the local site.
1611 :param graph: the intersite graph
1612 :param detect_failed: whether to detect failed links
1613 :return: True if some bridgeheads were not found
1615 # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1616 # here, but using vertex seems to make more sense. That is,
1617 # the docs want this:
1619 #bh = self.get_bridgehead(local_vertex.site, vertex.part, transport,
1620 # local_vertex.is_black(), detect_failed)
1624 vertex.accept_red_red = []
1625 vertex.accept_black = []
1626 found_failed = False
1628 if vertex in graph.connected_vertices:
1629 t_guid = str(self.ip_transport.guid)
1631 bh = self.get_bridgehead(vertex.site, vertex.part,
1633 vertex.is_black(), detect_failed)
1635 if vertex.site.is_rodc_site():
1636 vertex.accept_red_red.append(t_guid)
1640 vertex.accept_red_red.append(t_guid)
1641 vertex.accept_black.append(t_guid)
1643 # Add additional transport to ensure another run of Dijkstra
1644 vertex.accept_red_red.append("EDGE_TYPE_ALL")
1645 vertex.accept_black.append("EDGE_TYPE_ALL")
1649 def create_connections(self, graph, part, detect_failed):
1650 """Create intersite NTDSConnections as needed by a partition
1652 Construct an NC replica graph for the NC identified by
1653 the given crossRef, then create any additional nTDSConnection
1656 :param graph: site graph.
1657 :param part: crossRef object for NC.
1658 :param detect_failed: True to detect failed DCs and route
1659 replication traffic around them, False to assume no DC
1662 Modifies self.kept_connections by adding any connections
1663 deemed to be "in use".
1665 :return: (all_connected, found_failed_dc)
1666 (all_connected) True if the resulting NC replica graph
1667 connects all sites that need to be connected.
1668 (found_failed_dc) True if one or more failed DCs were
1671 all_connected = True
1672 found_failed = False
1674 DEBUG_FN("create_connections(): enter\n"
1675 "\tpartdn=%s\n\tdetect_failed=%s" %
1676 (part.nc_dnstr, detect_failed))
1678 # XXX - This is a highly abbreviated function from the MS-TECH
1679 # ref. It creates connections between bridgeheads to all
1680 # sites that have appropriate replicas. Thus we are not
1681 # creating a minimum cost spanning tree but instead
1682 # producing a fully connected tree. This should produce
1683 # a full (albeit not optimal cost) replication topology.
1685 my_vertex = Vertex(self.my_site, part)
1686 my_vertex.color_vertex()
1688 for v in graph.vertices:
1690 if self.add_transports(v, my_vertex, graph, detect_failed):
1693 # No NC replicas for this NC in the site of the local DC,
1694 # so no nTDSConnection objects need be created
1695 if my_vertex.is_white():
1696 return all_connected, found_failed
1698 edge_list, n_components = get_spanning_tree_edges(graph,
1702 DEBUG_FN("%s Number of components: %d" %
1703 (part.nc_dnstr, n_components))
1704 if n_components > 1:
1705 all_connected = False
1707 # LET partialReplicaOkay be TRUE if and only if
1708 # localSiteVertex.Color = COLOR.BLACK
1709 partial_ok = my_vertex.is_black()
1711 # Utilize the IP transport only for now
1712 transport = self.ip_transport
1714 DEBUG("edge_list %s" % edge_list)
1716 # XXX more accurate comparison?
1717 if e.directed and e.vertices[0].site is self.my_site:
1720 if e.vertices[0].site is self.my_site:
1721 rsite = e.vertices[1].site
1723 rsite = e.vertices[0].site
1725 # We don't make connections to our own site as that
1726 # is intrasite topology generator's job
1727 if rsite is self.my_site:
1728 DEBUG("rsite is my_site")
1731 # Determine bridgehead server in remote site
1732 rbh = self.get_bridgehead(rsite, part, transport,
1733 partial_ok, detect_failed)
1737 # RODC acts as an BH for itself
1739 # LET lbh be the nTDSDSA object of the local DC
1741 # LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1742 # cr, t, partialReplicaOkay, detectFailedDCs)
1743 if self.my_dsa.is_ro():
1744 lsite = self.my_site
1747 lsite = self.my_site
1748 lbh = self.get_bridgehead(lsite, part, transport,
1749 partial_ok, detect_failed)
1752 debug.DEBUG_RED("DISASTER! lbh is None")
1755 DEBUG_FN("lsite: %s\nrsite: %s" % (lsite, rsite))
1756 DEBUG_FN("vertices %s" % (e.vertices,))
1757 debug.DEBUG_BLUE("bridgeheads\n%s\n%s\n%s" % (lbh, rbh, "-" * 70))
1759 sitelink = e.site_link
1760 if sitelink is None:
1764 link_opt = sitelink.options
1765 link_sched = sitelink.schedule
1767 self.create_connection(part, rbh, rsite, transport,
1768 lbh, lsite, link_opt, link_sched,
1769 partial_ok, detect_failed)
1771 return all_connected, found_failed
1773 def create_intersite_connections(self):
1774 """Create NTDSConnections as necessary for all partitions.
1776 Computes an NC replica graph for each NC replica that "should be
1777 present" on the local DC or "is present" on any DC in the same site
1778 as the local DC. For each edge directed to an NC replica on such a
1779 DC from an NC replica on a DC in another site, the KCC creates an
1780 nTDSConnection object to imply that edge if one does not already
1783 Modifies self.kept_connections - A set of nTDSConnection
1784 objects for edges that are directed
1785 to the local DC's site in one or more NC replica graphs.
1787 :return: True if spanning trees were created for all NC replica
1788 graphs, otherwise False.
1790 all_connected = True
1791 self.kept_connections = set()
1793 # LET crossRefList be the set containing each object o of class
1794 # crossRef such that o is a child of the CN=Partitions child of the
1797 # FOR each crossRef object cr in crossRefList
1798 # IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1799 # is clear in cr!systemFlags, skip cr.
1800 # LET g be the GRAPH return of SetupGraph()
1802 for part in self.part_table.values():
1804 if not part.is_enabled():
1807 if part.is_foreign():
1810 graph = self.setup_graph(part)
1812 # Create nTDSConnection objects, routing replication traffic
1813 # around "failed" DCs.
1814 found_failed = False
1816 connected, found_failed = self.create_connections(graph,
1819 DEBUG("with detect_failed: connected %s Found failed %s" %
1820 (connected, found_failed))
1822 all_connected = False
1825 # One or more failed DCs preclude use of the ideal NC
1826 # replica graph. Add connections for the ideal graph.
1827 self.create_connections(graph, part, False)
1829 return all_connected
1831 def intersite(self, ping):
1832 """Generate the inter-site KCC replica graph and nTDSConnections
1834 As per MS-ADTS 6.2.2.3.
1836 If self.readonly is False, the connections are added to self.samdb.
1838 Produces self.kept_connections which is a set of NTDS
1839 Connections that should be kept during subsequent pruning
1842 After this has run, all sites should be connected in a minimum
1845 :param ping: An oracle function of remote site availability
1846 :return (True or False): (True) if the produced NC replica
1847 graph connects all sites that need to be connected
1852 mysite = self.my_site
1853 all_connected = True
1855 DEBUG_FN("intersite(): enter")
1857 # Determine who is the ISTG
1859 mysite.select_istg(self.samdb, mydsa, ro=True)
1861 mysite.select_istg(self.samdb, mydsa, ro=False)
1863 # Test whether local site has topology disabled
1864 if mysite.is_intersite_topology_disabled():
1865 DEBUG_FN("intersite(): exit disabled all_connected=%d" %
1867 return all_connected
1869 if not mydsa.is_istg():
1870 DEBUG_FN("intersite(): exit not istg all_connected=%d" %
1872 return all_connected
1874 self.merge_failed_links(ping)
1876 # For each NC with an NC replica that "should be present" on the
1877 # local DC or "is present" on any DC in the same site as the
1878 # local DC, the KCC constructs a site graph--a precursor to an NC
1879 # replica graph. The site connectivity for a site graph is defined
1880 # by objects of class interSiteTransport, siteLink, and
1881 # siteLinkBridge in the config NC.
1883 all_connected = self.create_intersite_connections()
1885 DEBUG_FN("intersite(): exit all_connected=%d" % all_connected)
1886 return all_connected
1888 # This function currently does no actions. The reason being that we cannot
1889 # perform modifies in this way on the RODC.
1890 def update_rodc_connection(self, ro=True):
1891 """Updates the RODC NTFRS connection object.
1893 If the local DSA is not an RODC, this does nothing.
1895 if not self.my_dsa.is_ro():
1898 # Given an nTDSConnection object cn1, such that cn1.options contains
1899 # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1900 # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1901 # that the following is true:
1903 # cn1.fromServer = cn2.fromServer
1904 # cn1.schedule = cn2.schedule
1906 # If no such cn2 can be found, cn1 is not modified.
1907 # If no such cn1 can be found, nothing is modified by this task.
1909 all_connections = self.my_dsa.connect_table.values()
1910 ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1911 rw_connections = [x for x in all_connections
1912 if x not in ro_connections]
1914 # XXX here we are dealing with multiple RODC_TOPO connections,
1915 # if they exist. It is not clear whether the spec means that
1916 # or if it ever arises.
1917 if rw_connections and ro_connections:
1918 for con in ro_connections:
1919 cn2 = rw_connections[0]
1920 con.from_dnstr = cn2.from_dnstr
1921 con.schedule = cn2.schedule
1922 con.to_be_modified = True
1924 self.my_dsa.commit_connections(self.samdb, ro=ro)
1926 def intrasite_max_node_edges(self, node_count):
1927 """Find the maximum number of edges directed to an intrasite node
1929 The KCC does not create more than 50 edges directed to a
1930 single DC. To optimize replication, we compute that each node
1931 should have n+2 total edges directed to it such that (n) is
1932 the smallest non-negative integer satisfying
1933 (node_count <= 2*(n*n) + 6*n + 7)
1935 (If the number of edges is m (i.e. n + 2), that is the same as
1936 2 * m*m - 2 * m + 3). We think in terms of n because that is
1937 the number of extra connections over the double directed ring
1938 that exists by default.
1948 :param node_count: total number of nodes in the replica graph
1950 The intention is that there should be no more than 3 hops
1951 between any two DSAs at a site. With up to 7 nodes the 2 edges
1952 of the ring are enough; any configuration of extra edges with
1953 8 nodes will be enough. It is less clear that the 3 hop
1954 guarantee holds at e.g. 15 nodes in degenerate cases, but
1955 those are quite unlikely given the extra edges are randomly
1958 :param node_count: the number of nodes in the site
1959 "return: The desired maximum number of connections
1963 if node_count <= (2 * (n * n) + (6 * n) + 7):
1971 def construct_intrasite_graph(self, site_local, dc_local,
1972 nc_x, gc_only, detect_stale):
1973 """Create an intrasite graph using given parameters
1975 This might be called a number of times per site with different
1978 Based on [MS-ADTS] 6.2.2.2
1980 :param site_local: site for which we are working
1981 :param dc_local: local DC that potentially needs a replica
1982 :param nc_x: naming context (x) that we are testing if it
1983 "should be present" on the local DC
1984 :param gc_only: Boolean - only consider global catalog servers
1985 :param detect_stale: Boolean - check whether links seems down
1988 # We're using the MS notation names here to allow
1989 # correlation back to the published algorithm.
1991 # nc_x - naming context (x) that we are testing if it
1992 # "should be present" on the local DC
1993 # f_of_x - replica (f) found on a DC (s) for NC (x)
1994 # dc_s - DC where f_of_x replica was found
1995 # dc_local - local DC that potentially needs a replica
1997 # r_list - replica list R
1998 # p_of_x - replica (p) is partial and found on a DC (s)
2000 # l_of_x - replica (l) is the local replica for NC (x)
2001 # that should appear on the local DC
2002 # r_len = is length of replica list |R|
2004 # If the DSA doesn't need a replica for this
2005 # partition (NC x) then continue
2006 needed, ro, partial = nc_x.should_be_present(dc_local)
2008 debug.DEBUG_YELLOW("construct_intrasite_graph(): enter" +
2009 "\n\tgc_only=%d" % gc_only +
2010 "\n\tdetect_stale=%d" % detect_stale +
2011 "\n\tneeded=%s" % needed +
2013 "\n\tpartial=%s" % partial +
2017 debug.DEBUG_RED("%s lacks 'should be present' status, "
2018 "aborting construct_intrasite_graph!" %
2022 # Create a NCReplica that matches what the local replica
2023 # should say. We'll use this below in our r_list
2024 l_of_x = NCReplica(dc_local, nc_x.nc_dnstr)
2026 l_of_x.identify_by_basedn(self.samdb)
2028 l_of_x.rep_partial = partial
2031 # Add this replica that "should be present" to the
2032 # needed replica table for this DSA
2033 dc_local.add_needed_replica(l_of_x)
2037 # Let R be a sequence containing each writable replica f of x
2038 # such that f "is present" on a DC s satisfying the following
2041 # * s is a writable DC other than the local DC.
2043 # * s is in the same site as the local DC.
2045 # * If x is a read-only full replica and x is a domain NC,
2046 # then the DC's functional level is at least
2047 # DS_BEHAVIOR_WIN2008.
2049 # * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
2050 # in the options attribute of the site settings object for
2051 # the local DC's site, or no tuple z exists in the
2052 # kCCFailedLinks or kCCFailedConnections variables such
2053 # that z.UUIDDsa is the objectGUID of the nTDSDSA object
2054 # for s, z.FailureCount > 0, and the current time -
2055 # z.TimeFirstFailure > 2 hours.
2059 # We'll loop thru all the DSAs looking for
2060 # writeable NC replicas that match the naming
2061 # context dn for (nc_x)
2063 for dc_s in self.my_site.dsa_table.values():
2064 # If this partition (nc_x) doesn't appear as a
2065 # replica (f_of_x) on (dc_s) then continue
2066 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2069 # Pull out the NCReplica (f) of (x) with the dn
2070 # that matches NC (x) we are examining.
2071 f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2073 # Replica (f) of NC (x) must be writable
2077 # Replica (f) of NC (x) must satisfy the
2078 # "is present" criteria for DC (s) that
2080 if not f_of_x.is_present():
2083 # DC (s) must be a writable DSA other than
2084 # my local DC. In other words we'd only replicate
2085 # from other writable DC
2086 if dc_s.is_ro() or dc_s is dc_local:
2089 # Certain replica graphs are produced only
2090 # for global catalogs, so test against
2091 # method input parameter
2092 if gc_only and not dc_s.is_gc():
2095 # DC (s) must be in the same site as the local DC
2096 # as this is the intra-site algorithm. This is
2097 # handled by virtue of placing DSAs in per
2098 # site objects (see enclosing for() loop)
2100 # If NC (x) is intended to be read-only full replica
2101 # for a domain NC on the target DC then the source
2102 # DC should have functional level at minimum WIN2008
2104 # Effectively we're saying that in order to replicate
2105 # to a targeted RODC (which was introduced in Windows 2008)
2106 # then we have to replicate from a DC that is also minimally
2109 # You can also see this requirement in the MS special
2110 # considerations for RODC which state that to deploy
2111 # an RODC, at least one writable domain controller in
2112 # the domain must be running Windows Server 2008
2113 if ro and not partial and nc_x.nc_type == NCType.domain:
2114 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2117 # If we haven't been told to turn off stale connection
2118 # detection and this dsa has a stale connection then
2120 if detect_stale and self.is_stale_link_connection(dc_s):
2123 # Replica meets criteria. Add it to table indexed
2124 # by the GUID of the DC that it appears on
2125 r_list.append(f_of_x)
2127 # If a partial (not full) replica of NC (x) "should be present"
2128 # on the local DC, append to R each partial replica (p of x)
2129 # such that p "is present" on a DC satisfying the same
2130 # criteria defined above for full replica DCs.
2132 # XXX This loop and the previous one differ only in whether
2133 # the replica is partial or not. here we only accept partial
2134 # (because we're partial); before we only accepted full. Order
2135 # doen't matter (the list is sorted a few lines down) so these
2136 # loops could easily be merged. Or this could be a helper
2140 # Now we loop thru all the DSAs looking for
2141 # partial NC replicas that match the naming
2142 # context dn for (NC x)
2143 for dc_s in self.my_site.dsa_table.values():
2145 # If this partition NC (x) doesn't appear as a
2146 # replica (p) of NC (x) on the dsa DC (s) then
2148 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2151 # Pull out the NCReplica with the dn that
2152 # matches NC (x) we are examining.
2153 p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2155 # Replica (p) of NC (x) must be partial
2156 if not p_of_x.is_partial():
2159 # Replica (p) of NC (x) must satisfy the
2160 # "is present" criteria for DC (s) that
2162 if not p_of_x.is_present():
2165 # DC (s) must be a writable DSA other than
2166 # my DSA. In other words we'd only replicate
2167 # from other writable DSA
2168 if dc_s.is_ro() or dc_s is dc_local:
2171 # Certain replica graphs are produced only
2172 # for global catalogs, so test against
2173 # method input parameter
2174 if gc_only and not dc_s.is_gc():
2177 # If we haven't been told to turn off stale connection
2178 # detection and this dsa has a stale connection then
2180 if detect_stale and self.is_stale_link_connection(dc_s):
2183 # Replica meets criteria. Add it to table indexed
2184 # by the GUID of the DSA that it appears on
2185 r_list.append(p_of_x)
2187 # Append to R the NC replica that "should be present"
2189 r_list.append(l_of_x)
2191 r_list.sort(sort_replica_by_dsa_guid)
2194 max_node_edges = self.intrasite_max_node_edges(r_len)
2196 # Add a node for each r_list element to the replica graph
2199 node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2200 graph_list.append(node)
2202 # For each r(i) from (0 <= i < |R|-1)
2204 while i < (r_len-1):
2205 # Add an edge from r(i) to r(i+1) if r(i) is a full
2206 # replica or r(i+1) is a partial replica
2207 if not r_list[i].is_partial() or r_list[i+1].is_partial():
2208 graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
2210 # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2211 # replica or ri is a partial replica.
2212 if not r_list[i+1].is_partial() or r_list[i].is_partial():
2213 graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
2216 # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2217 # or r0 is a partial replica.
2218 if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
2219 graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
2221 # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2222 # r|R|-1 is a partial replica.
2223 if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
2224 graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
2226 DEBUG("r_list is length %s" % len(r_list))
2227 DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr))
2230 do_dot_files = self.dot_file_dir is not None and self.debug
2231 if self.verify or do_dot_files:
2233 dot_vertices = set()
2234 for v1 in graph_list:
2235 dot_vertices.add(v1.dsa_dnstr)
2236 for v2 in v1.edge_from:
2237 dot_edges.append((v2, v1.dsa_dnstr))
2238 dot_vertices.add(v2)
2240 verify_properties = ('connected',)
2241 verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2242 label='%s__%s__%s' % (site_local.site_dnstr,
2243 nctype_lut[nc_x.nc_type],
2245 properties=verify_properties, debug=DEBUG,
2247 dot_file_dir=self.dot_file_dir,
2250 rw_dot_vertices = set(x for x in dot_vertices
2251 if not self.get_dsa(x).is_ro())
2252 rw_dot_edges = [(a, b) for a, b in dot_edges if
2253 a in rw_dot_vertices and b in rw_dot_vertices]
2254 rw_verify_properties = ('connected',
2255 'directed_double_ring_or_small')
2256 verify_and_dot('intrasite_rw_pre_ntdscon', rw_dot_edges,
2258 label='%s__%s__%s' % (site_local.site_dnstr,
2259 nctype_lut[nc_x.nc_type],
2261 properties=rw_verify_properties, debug=DEBUG,
2263 dot_file_dir=self.dot_file_dir,
2266 # For each existing nTDSConnection object implying an edge
2267 # from rj of R to ri such that j != i, an edge from rj to ri
2268 # is not already in the graph, and the total edges directed
2269 # to ri is less than n+2, the KCC adds that edge to the graph.
2270 for vertex in graph_list:
2271 dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2272 for connect in dsa.connect_table.values():
2273 remote = connect.from_dnstr
2274 if remote in self.my_site.dsa_table:
2275 vertex.add_edge_from(remote)
2277 DEBUG('reps are: %s' % ' '.join(x.rep_dsa_dnstr for x in r_list))
2278 DEBUG('dsas are: %s' % ' '.join(x.dsa_dnstr for x in graph_list))
2280 for tnode in graph_list:
2281 # To optimize replication latency in sites with many NC
2282 # replicas, the KCC adds new edges directed to ri to bring
2283 # the total edges to n+2, where the NC replica rk of R
2284 # from which the edge is directed is chosen at random such
2285 # that k != i and an edge from rk to ri is not already in
2288 # Note that the KCC tech ref does not give a number for
2289 # the definition of "sites with many NC replicas". At a
2290 # bare minimum to satisfy n+2 edges directed at a node we
2291 # have to have at least three replicas in |R| (i.e. if n
2292 # is zero then at least replicas from two other graph
2293 # nodes may direct edges to us).
2294 if r_len >= 3 and not tnode.has_sufficient_edges():
2295 candidates = [x for x in graph_list if
2297 x.dsa_dnstr not in tnode.edge_from)]
2299 debug.DEBUG_BLUE("looking for random link for %s. r_len %d, "
2300 "graph len %d candidates %d"
2301 % (tnode.dsa_dnstr, r_len, len(graph_list),
2304 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2306 while candidates and not tnode.has_sufficient_edges():
2307 other = random.choice(candidates)
2308 DEBUG("trying to add candidate %s" % other.dsa_dnstr)
2309 if not tnode.add_edge_from(other.dsa_dnstr):
2310 debug.DEBUG_RED("could not add %s" % other.dsa_dnstr)
2311 candidates.remove(other)
2313 DEBUG_FN("not adding links to %s: nodes %s, links is %s/%s" %
2314 (tnode.dsa_dnstr, r_len, len(tnode.edge_from),
2317 # Print the graph node in debug mode
2318 DEBUG_FN("%s" % tnode)
2320 # For each edge directed to the local DC, ensure a nTDSConnection
2321 # points to us that satisfies the KCC criteria
2323 if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2324 tnode.add_connections_from_edges(dc_local, self.ip_transport)
2326 if self.verify or do_dot_files:
2328 dot_vertices = set()
2329 for v1 in graph_list:
2330 dot_vertices.add(v1.dsa_dnstr)
2331 for v2 in v1.edge_from:
2332 dot_edges.append((v2, v1.dsa_dnstr))
2333 dot_vertices.add(v2)
2335 verify_properties = ('connected',)
2336 verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2337 label='%s__%s__%s' % (site_local.site_dnstr,
2338 nctype_lut[nc_x.nc_type],
2340 properties=verify_properties, debug=DEBUG,
2342 dot_file_dir=self.dot_file_dir,
2345 rw_dot_vertices = set(x for x in dot_vertices
2346 if not self.get_dsa(x).is_ro())
2347 rw_dot_edges = [(a, b) for a, b in dot_edges if
2348 a in rw_dot_vertices and b in rw_dot_vertices]
2349 rw_verify_properties = ('connected',
2350 'directed_double_ring_or_small')
2351 verify_and_dot('intrasite_rw_post_ntdscon', rw_dot_edges,
2353 label='%s__%s__%s' % (site_local.site_dnstr,
2354 nctype_lut[nc_x.nc_type],
2356 properties=rw_verify_properties, debug=DEBUG,
2358 dot_file_dir=self.dot_file_dir,
2361 def intrasite(self):
2362 """Generate the intrasite KCC connections
2364 As per MS-ADTS 6.2.2.2.
2366 If self.readonly is False, the connections are added to self.samdb.
2368 After this call, all DCs in each site with more than 3 DCs
2369 should be connected in a bidirectional ring. If a site has 2
2370 DCs, they will bidirectionally connected. Sites with many DCs
2371 may have arbitrary extra connections.
2377 DEBUG_FN("intrasite(): enter")
2379 # Test whether local site has topology disabled
2380 mysite = self.my_site
2381 if mysite.is_intrasite_topology_disabled():
2384 detect_stale = (not mysite.is_detect_stale_disabled())
2385 for connect in mydsa.connect_table.values():
2386 if connect.to_be_added:
2387 debug.DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2389 # Loop thru all the partitions, with gc_only False
2390 for partdn, part in self.part_table.items():
2391 self.construct_intrasite_graph(mysite, mydsa, part, False,
2393 for connect in mydsa.connect_table.values():
2394 if connect.to_be_added:
2395 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2397 # If the DC is a GC server, the KCC constructs an additional NC
2398 # replica graph (and creates nTDSConnection objects) for the
2399 # config NC as above, except that only NC replicas that "are present"
2400 # on GC servers are added to R.
2401 for connect in mydsa.connect_table.values():
2402 if connect.to_be_added:
2403 debug.DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2405 # Do it again, with gc_only True
2406 for partdn, part in self.part_table.items():
2407 if part.is_config():
2408 self.construct_intrasite_graph(mysite, mydsa, part, True,
2411 # The DC repeats the NC replica graph computation and nTDSConnection
2412 # creation for each of the NC replica graphs, this time assuming
2413 # that no DC has failed. It does so by re-executing the steps as
2414 # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2415 # set in the options attribute of the site settings object for
2416 # the local DC's site. (ie. we set "detec_stale" flag to False)
2417 for connect in mydsa.connect_table.values():
2418 if connect.to_be_added:
2419 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2421 # Loop thru all the partitions.
2422 for partdn, part in self.part_table.items():
2423 self.construct_intrasite_graph(mysite, mydsa, part, False,
2424 False) # don't detect stale
2426 # If the DC is a GC server, the KCC constructs an additional NC
2427 # replica graph (and creates nTDSConnection objects) for the
2428 # config NC as above, except that only NC replicas that "are present"
2429 # on GC servers are added to R.
2430 for connect in mydsa.connect_table.values():
2431 if connect.to_be_added:
2432 debug.DEBUG_RED("TO BE ADDED:\n%s" % connect)
2434 for partdn, part in self.part_table.items():
2435 if part.is_config():
2436 self.construct_intrasite_graph(mysite, mydsa, part, True,
2437 False) # don't detect stale
2439 self._commit_changes(mydsa)
2441 def list_dsas(self):
2442 """Compile a comprehensive list of DSA DNs
2444 These are all the DSAs on all the sites that KCC would be
2447 This method is not idempotent and may not work correctly in
2448 sequence with KCC.run().
2450 :return: a list of DSA DN strings.
2455 self.load_all_sites()
2456 self.load_all_partitions()
2457 self.load_ip_transport()
2458 self.load_all_sitelinks()
2460 for site in self.site_table.values():
2461 dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2462 for dsa in site.dsa_table.values()])
2465 def load_samdb(self, dburl, lp, creds, force=False):
2466 """Load the database using an url, loadparm, and credentials
2468 If force is False, the samdb won't be reloaded if it already
2471 :param dburl: a database url.
2472 :param lp: a loadparm object.
2473 :param creds: a Credentials object.
2474 :param force: a boolean indicating whether to overwrite.
2477 if force or self.samdb is None:
2479 self.samdb = SamDB(url=dburl,
2480 session_info=system_session(),
2481 credentials=creds, lp=lp)
2482 except ldb.LdbError, (num, msg):
2483 raise KCCError("Unable to open sam database %s : %s" %
2486 def plot_all_connections(self, basename, verify_properties=()):
2487 """Helper function to plot and verify NTDSConnections
2489 :param basename: an identifying string to use in filenames and logs.
2490 :param verify_properties: properties to verify (default empty)
2492 verify = verify_properties and self.verify
2493 if not verify and self.dot_file_dir is None:
2501 for dsa in self.dsa_by_dnstr.values():
2502 dot_vertices.append(dsa.dsa_dnstr)
2504 vertex_colours.append('#cc0000')
2506 vertex_colours.append('#0000cc')
2507 for con in dsa.connect_table.values():
2508 if con.is_rodc_topology():
2509 edge_colours.append('red')
2511 edge_colours.append('blue')
2512 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2514 verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2515 label=self.my_dsa_dnstr,
2516 properties=verify_properties, debug=DEBUG,
2517 verify=verify, dot_file_dir=self.dot_file_dir,
2518 directed=True, edge_colors=edge_colours,
2519 vertex_colors=vertex_colours)
2521 def run(self, dburl, lp, creds, forced_local_dsa=None,
2522 forget_local_links=False, forget_intersite_links=False,
2523 attempt_live_connections=False):
2524 """Perform a KCC run, possibly updating repsFrom topology
2526 :param dburl: url of the database to work with.
2527 :param lp: a loadparm object.
2528 :param creds: a Credentials object.
2529 :param forced_local_dsa: pretend to be on the DSA with this dn_str
2530 :param forget_local_links: calculate as if no connections existed
2531 (boolean, default False)
2532 :param forget_intersite_links: calculate with only intrasite connection
2533 (boolean, default False)
2534 :param attempt_live_connections: attempt to connect to remote DSAs to
2535 determine link availability (boolean, default False)
2536 :return: 1 on error, 0 otherwise
2538 if self.samdb is None:
2539 DEBUG_FN("samdb is None; let's load it from %s" % (dburl,))
2540 self.load_samdb(dburl, lp, creds, force=False)
2542 if forced_local_dsa:
2543 self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" %
2551 self.load_all_sites()
2552 self.load_all_partitions()
2553 self.load_ip_transport()
2554 self.load_all_sitelinks()
2556 if self.verify or self.dot_file_dir is not None:
2558 for site in self.site_table.values():
2559 guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2561 in site.dsa_table.items())
2563 self.plot_all_connections('dsa_initial')
2566 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2567 for dnstr, c_rep in current_reps.items():
2568 DEBUG("c_rep %s" % c_rep)
2569 dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2571 verify_and_dot('dsa_repsFrom_initial', dot_edges,
2572 directed=True, label=self.my_dsa_dnstr,
2573 properties=(), debug=DEBUG, verify=self.verify,
2574 dot_file_dir=self.dot_file_dir)
2577 for site in self.site_table.values():
2578 for dsa in site.dsa_table.values():
2579 current_reps, needed_reps = dsa.get_rep_tables()
2580 for dn_str, rep in current_reps.items():
2581 for reps_from in rep.rep_repsFrom:
2582 DEBUG("rep %s" % rep)
2583 dsa_guid = str(reps_from.source_dsa_obj_guid)
2584 dsa_dn = guid_to_dnstr[dsa_guid]
2585 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2587 verify_and_dot('dsa_repsFrom_initial_all', dot_edges,
2588 directed=True, label=self.my_dsa_dnstr,
2589 properties=(), debug=DEBUG, verify=self.verify,
2590 dot_file_dir=self.dot_file_dir)
2593 for link in self.sitelink_table.values():
2594 for a, b in itertools.combinations(link.site_list, 2):
2595 dot_edges.append((str(a), str(b)))
2596 properties = ('connected',)
2597 verify_and_dot('dsa_sitelink_initial', dot_edges,
2599 label=self.my_dsa_dnstr, properties=properties,
2600 debug=DEBUG, verify=self.verify,
2601 dot_file_dir=self.dot_file_dir)
2603 if forget_local_links:
2604 for dsa in self.my_site.dsa_table.values():
2605 dsa.connect_table = dict((k, v) for k, v in
2606 dsa.connect_table.items()
2607 if v.is_rodc_topology() or
2608 (v.from_dnstr not in
2609 self.my_site.dsa_table))
2610 self.plot_all_connections('dsa_forgotten_local')
2612 if forget_intersite_links:
2613 for site in self.site_table.values():
2614 for dsa in site.dsa_table.values():
2615 dsa.connect_table = dict((k, v) for k, v in
2616 dsa.connect_table.items()
2617 if site is self.my_site and
2618 v.is_rodc_topology())
2620 self.plot_all_connections('dsa_forgotten_all')
2622 if attempt_live_connections:
2623 # Encapsulates lp and creds in a function that
2624 # attempts connections to remote DSAs.
2625 def ping(self, dnsname):
2627 drs_utils.drsuapi_connect(dnsname, self.lp, self.creds)
2628 except drs_utils.drsException:
2633 # These are the published steps (in order) for the
2634 # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2637 self.refresh_failed_links_connections(ping)
2643 all_connected = self.intersite(ping)
2646 self.remove_unneeded_ntdsconn(all_connected)
2649 self.translate_ntdsconn()
2652 self.remove_unneeded_failed_links_connections()
2655 self.update_rodc_connection()
2657 if self.verify or self.dot_file_dir is not None:
2658 self.plot_all_connections('dsa_final',
2661 debug.DEBUG_MAGENTA("there are %d dsa guids" %
2666 my_dnstr = self.my_dsa.dsa_dnstr
2667 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2668 for dnstr, n_rep in needed_reps.items():
2669 for reps_from in n_rep.rep_repsFrom:
2670 guid_str = str(reps_from.source_dsa_obj_guid)
2671 dot_edges.append((my_dnstr, guid_to_dnstr[guid_str]))
2672 edge_colors.append('#' + str(n_rep.nc_guid)[:6])
2674 verify_and_dot('dsa_repsFrom_final', dot_edges, directed=True,
2675 label=self.my_dsa_dnstr,
2676 properties=(), debug=DEBUG, verify=self.verify,
2677 dot_file_dir=self.dot_file_dir,
2678 edge_colors=edge_colors)
2682 for site in self.site_table.values():
2683 for dsa in site.dsa_table.values():
2684 current_reps, needed_reps = dsa.get_rep_tables()
2685 for n_rep in needed_reps.values():
2686 for reps_from in n_rep.rep_repsFrom:
2687 dsa_guid = str(reps_from.source_dsa_obj_guid)
2688 dsa_dn = guid_to_dnstr[dsa_guid]
2689 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2691 verify_and_dot('dsa_repsFrom_final_all', dot_edges,
2692 directed=True, label=self.my_dsa_dnstr,
2693 properties=(), debug=DEBUG, verify=self.verify,
2694 dot_file_dir=self.dot_file_dir)
2701 def import_ldif(self, dburl, lp, ldif_file, forced_local_dsa=None):
2702 """Import relevant objects and attributes from an LDIF file.
2704 The point of this function is to allow a programmer/debugger to
2705 import an LDIF file with non-security relevent information that
2706 was previously extracted from a DC database. The LDIF file is used
2707 to create a temporary abbreviated database. The KCC algorithm can
2708 then run against this abbreviated database for debug or test
2709 verification that the topology generated is computationally the
2710 same between different OSes and algorithms.
2712 :param dburl: path to the temporary abbreviated db to create
2713 :param lp: a loadparm object.
2714 :param ldif_file: path to the ldif file to import
2715 :param forced_local_dsa: perform KCC from this DSA's point of view
2716 :return: zero on success, 1 on error
2719 self.samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif_file,
2721 except ldif_import_export.LdifError, e:
2726 def export_ldif(self, dburl, lp, creds, ldif_file):
2727 """Save KCC relevant details to an ldif file
2729 The point of this function is to allow a programmer/debugger to
2730 extract an LDIF file with non-security relevent information from
2731 a DC database. The LDIF file can then be used to "import" via
2732 the import_ldif() function this file into a temporary abbreviated
2733 database. The KCC algorithm can then run against this abbreviated
2734 database for debug or test verification that the topology generated
2735 is computationally the same between different OSes and algorithms.
2737 :param dburl: LDAP database URL to extract info from
2738 :param lp: a loadparm object.
2739 :param cred: a Credentials object.
2740 :param ldif_file: output LDIF file name to create
2741 :return: zero on success, 1 on error
2744 ldif_import_export.samdb_to_ldif_file(self.samdb, dburl, lp, creds,
2746 except ldif_import_export.LdifError, e: