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
47 from samba.compat import text_type
50 def sort_dsa_by_gc_and_guid(dsa1, dsa2):
51 """Helper to sort DSAs by guid global catalog status
53 GC DSAs come before non-GC DSAs, other than that, the guids are
56 :param dsa1: A DSA object
57 :param dsa2: Another DSA
58 :return: -1, 0, or 1, indicating sort order.
60 if dsa1.is_gc() and not dsa2.is_gc():
62 if not dsa1.is_gc() and dsa2.is_gc():
64 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
67 def is_smtp_replication_available():
68 """Can the KCC use SMTP replication?
70 Currently always returns false because Samba doesn't implement
71 SMTP transfer for NC changes between DCs.
73 :return: Boolean (always False)
79 """The Knowledge Consistency Checker class.
81 A container for objects and methods allowing a run of the KCC. Produces a
82 set of connections in the samdb for which the Distributed Replication
83 Service can then utilize to replicate naming contexts
85 :param unix_now: The putative current time in seconds since 1970.
86 :param readonly: Don't write to the database.
87 :param verify: Check topological invariants for the generated graphs
88 :param debug: Write verbosely to stderr.
89 :param dot_file_dir: write diagnostic Graphviz files in this directory
91 def __init__(self, unix_now, readonly=False, verify=False, debug=False,
93 """Initializes the partitions class which can hold
94 our local DCs partitions or all the partitions in
97 self.part_table = {} # partition objects
99 self.ip_transport = None
100 self.sitelink_table = {}
101 self.dsa_by_dnstr = {}
102 self.dsa_by_guid = {}
104 self.get_dsa_by_guidstr = self.dsa_by_guid.get
105 self.get_dsa = self.dsa_by_dnstr.get
107 # TODO: These should be backed by a 'permanent' store so that when
108 # calling DRSGetReplInfo with DS_REPL_INFO_KCC_DSA_CONNECT_FAILURES,
109 # the failure information can be returned
110 self.kcc_failed_links = {}
111 self.kcc_failed_connections = set()
113 # Used in inter-site topology computation. A list
114 # of connections (by NTDSConnection object) that are
115 # to be kept when pruning un-needed NTDS Connections
116 self.kept_connections = set()
118 self.my_dsa_dnstr = None # My dsa DN
119 self.my_dsa = None # My dsa object
121 self.my_site_dnstr = None
126 self.unix_now = unix_now
127 self.nt_now = unix2nttime(unix_now)
128 self.readonly = readonly
131 self.dot_file_dir = dot_file_dir
133 def load_ip_transport(self):
134 """Loads the inter-site transport objects for Sites
137 :raise KCCError: if no IP transport is found
140 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
141 self.samdb.get_config_basedn(),
142 scope=ldb.SCOPE_SUBTREE,
143 expression="(objectClass=interSiteTransport)")
144 except ldb.LdbError as e2:
145 (enum, estr) = e2.args
146 raise KCCError("Unable to find inter-site transports - (%s)" %
152 transport = Transport(dnstr)
154 transport.load_transport(self.samdb)
155 if transport.name == 'IP':
156 self.ip_transport = transport
157 elif transport.name == 'SMTP':
158 logger.debug("Samba KCC is ignoring the obsolete "
162 logger.warning("Samba KCC does not support the transport "
163 "called %r." % (transport.name,))
165 if self.ip_transport is None:
166 raise KCCError("there doesn't seem to be an IP transport")
168 def load_all_sitelinks(self):
169 """Loads the inter-site siteLink objects
172 :raise KCCError: if site-links aren't found
175 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
176 self.samdb.get_config_basedn(),
177 scope=ldb.SCOPE_SUBTREE,
178 expression="(objectClass=siteLink)")
179 except ldb.LdbError as e3:
180 (enum, estr) = e3.args
181 raise KCCError("Unable to find inter-site siteLinks - (%s)" % estr)
187 if dnstr in self.sitelink_table:
190 sitelink = SiteLink(dnstr)
192 sitelink.load_sitelink(self.samdb)
194 # Assign this siteLink to table
196 self.sitelink_table[dnstr] = sitelink
198 def load_site(self, dn_str):
199 """Helper for load_my_site and load_all_sites.
201 Put all the site's DSAs into the KCC indices.
203 :param dn_str: a site dn_str
204 :return: the Site object pertaining to the dn_str
206 site = Site(dn_str, self.unix_now)
207 site.load_site(self.samdb)
209 # We avoid replacing the site with an identical copy in case
210 # somewhere else has a reference to the old one, which would
211 # lead to all manner of confusion and chaos.
212 guid = str(site.site_guid)
213 if guid not in self.site_table:
214 self.site_table[guid] = site
215 self.dsa_by_dnstr.update(site.dsa_table)
216 self.dsa_by_guid.update((str(x.dsa_guid), x)
217 for x in site.dsa_table.values())
219 return self.site_table[guid]
221 def load_my_site(self):
222 """Load the Site object for the local DSA.
226 self.my_site_dnstr = ("CN=%s,CN=Sites,%s" % (
227 self.samdb.server_site_name(),
228 self.samdb.get_config_basedn()))
230 self.my_site = self.load_site(self.my_site_dnstr)
232 def load_all_sites(self):
233 """Discover all sites and create Site objects.
236 :raise: KCCError if sites can't be found
239 res = self.samdb.search("CN=Sites,%s" %
240 self.samdb.get_config_basedn(),
241 scope=ldb.SCOPE_SUBTREE,
242 expression="(objectClass=site)")
243 except ldb.LdbError as e4:
244 (enum, estr) = e4.args
245 raise KCCError("Unable to find sites - (%s)" % estr)
248 sitestr = str(msg.dn)
249 self.load_site(sitestr)
251 def load_my_dsa(self):
252 """Discover my nTDSDSA dn thru the rootDSE entry
255 :raise: KCCError if DSA can't be found
257 dn_query = "<GUID=%s>" % self.samdb.get_ntds_GUID()
258 dn = ldb.Dn(self.samdb, dn_query)
260 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
261 attrs=["objectGUID"])
262 except ldb.LdbError as e5:
263 (enum, estr) = e5.args
264 DEBUG_FN("Search for dn '%s' [from %s] failed: %s. "
265 "This typically happens in --importldif mode due "
266 "to lack of module support." % (dn, dn_query, estr))
268 # We work around the failure above by looking at the
269 # dsServiceName that was put in the fake rootdse by
270 # the --exportldif, rather than the
271 # samdb.get_ntds_GUID(). The disadvantage is that this
272 # mode requires we modify the @ROOTDSE dnq to support
274 service_name_res = self.samdb.search(base="",
275 scope=ldb.SCOPE_BASE,
276 attrs=["dsServiceName"])
277 dn = ldb.Dn(self.samdb,
278 service_name_res[0]["dsServiceName"][0].decode('utf8'))
280 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
281 attrs=["objectGUID"])
282 except ldb.LdbError as e:
283 (enum, estr) = e.args
284 raise KCCError("Unable to find my nTDSDSA - (%s)" % estr)
287 raise KCCError("Unable to find my nTDSDSA at %s" %
290 ntds_guid = misc.GUID(self.samdb.get_ntds_GUID())
291 if misc.GUID(res[0]["objectGUID"][0]) != ntds_guid:
292 raise KCCError("Did not find the GUID we expected,"
293 " perhaps due to --importldif")
295 self.my_dsa_dnstr = str(res[0].dn)
297 self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
299 if self.my_dsa_dnstr not in self.dsa_by_dnstr:
300 debug.DEBUG_DARK_YELLOW("my_dsa %s isn't in self.dsas_by_dnstr:"
301 " it must be RODC.\n"
302 "Let's add it, because my_dsa is special!"
303 "\n(likewise for self.dsa_by_guid)" %
306 self.dsa_by_dnstr[self.my_dsa_dnstr] = self.my_dsa
307 self.dsa_by_guid[str(self.my_dsa.dsa_guid)] = self.my_dsa
309 def load_all_partitions(self):
310 """Discover and load all partitions.
312 Each NC is inserted into the part_table by partition
313 dn string (not the nCName dn string)
316 :raise: KCCError if partitions can't be found
319 res = self.samdb.search("CN=Partitions,%s" %
320 self.samdb.get_config_basedn(),
321 scope=ldb.SCOPE_SUBTREE,
322 expression="(objectClass=crossRef)")
323 except ldb.LdbError as e6:
324 (enum, estr) = e6.args
325 raise KCCError("Unable to find partitions - (%s)" % estr)
328 partstr = str(msg.dn)
331 if partstr in self.part_table:
334 part = Partition(partstr)
336 part.load_partition(self.samdb)
337 self.part_table[partstr] = part
339 def refresh_failed_links_connections(self, ping=None):
340 """Ensure the failed links list is up to date
342 Based on MS-ADTS 6.2.2.1
344 :param ping: An oracle function of remote site availability
347 # LINKS: Refresh failed links
348 self.kcc_failed_links = {}
349 current, needed = self.my_dsa.get_rep_tables()
350 for replica in current.values():
351 # For every possible connection to replicate
352 for reps_from in replica.rep_repsFrom:
353 failure_count = reps_from.consecutive_sync_failures
354 if failure_count <= 0:
357 dsa_guid = str(reps_from.source_dsa_obj_guid)
358 time_first_failure = reps_from.last_success
359 last_result = reps_from.last_attempt
360 dns_name = reps_from.dns_name1
362 f = self.kcc_failed_links.get(dsa_guid)
364 f = KCCFailedObject(dsa_guid, failure_count,
365 time_first_failure, last_result,
367 self.kcc_failed_links[dsa_guid] = f
369 f.failure_count = max(f.failure_count, failure_count)
370 f.time_first_failure = min(f.time_first_failure,
372 f.last_result = last_result
374 # CONNECTIONS: Refresh failed connections
375 restore_connections = set()
377 DEBUG("refresh_failed_links: checking if links are still down")
378 for connection in self.kcc_failed_connections:
379 if ping(connection.dns_name):
380 # Failed connection is no longer failing
381 restore_connections.add(connection)
383 connection.failure_count += 1
385 DEBUG("refresh_failed_links: not checking live links because we\n"
386 "weren't asked to --attempt-live-connections")
388 # Remove the restored connections from the failed connections
389 self.kcc_failed_connections.difference_update(restore_connections)
391 def is_stale_link_connection(self, target_dsa):
392 """Check whether a link to a remote DSA is stale
394 Used in MS-ADTS 6.2.2.2 Intrasite Connection Creation
396 Returns True if the remote seems to have been down for at
397 least two hours, otherwise False.
399 :param target_dsa: the remote DSA object
400 :return: True if link is stale, otherwise False
402 failed_link = self.kcc_failed_links.get(str(target_dsa.dsa_guid))
404 # failure_count should be > 0, but check anyways
405 if failed_link.failure_count > 0:
406 unix_first_failure = \
407 nttime2unix(failed_link.time_first_failure)
408 # TODO guard against future
409 if unix_first_failure > self.unix_now:
410 logger.error("The last success time attribute for \
411 repsFrom is in the future!")
413 # Perform calculation in seconds
414 if (self.unix_now - unix_first_failure) > 60 * 60 * 2:
418 # We have checked failed *links*, but we also need to check
423 # TODO: This should be backed by some form of local database
424 def remove_unneeded_failed_links_connections(self):
425 # Remove all tuples in kcc_failed_links where failure count = 0
426 # In this implementation, this should never happen.
428 # Remove all connections which were not used this run or connections
429 # that became active during this run.
432 def _ensure_connections_are_loaded(self, connections):
433 """Load or fake-load NTDSConnections lacking GUIDs
435 New connections don't have GUIDs and created times which are
436 needed for sorting. If we're in read-only mode, we make fake
437 GUIDs, otherwise we ask SamDB to do it for us.
439 :param connections: an iterable of NTDSConnection objects.
442 for cn_conn in connections:
443 if cn_conn.guid is None:
445 cn_conn.guid = misc.GUID(str(uuid.uuid4()))
446 cn_conn.whenCreated = self.nt_now
448 cn_conn.load_connection(self.samdb)
450 def _mark_broken_ntdsconn(self):
451 """Find NTDS Connections that lack a remote
453 I'm not sure how they appear. Let's be rid of them by marking
454 them with the to_be_deleted attribute.
458 for cn_conn in self.my_dsa.connect_table.values():
459 s_dnstr = cn_conn.get_from_dnstr()
461 DEBUG_FN("%s has phantom connection %s" % (self.my_dsa,
463 cn_conn.to_be_deleted = True
465 def _mark_unneeded_local_ntdsconn(self):
466 """Find unneeded intrasite NTDS Connections for removal
468 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections.
469 Every DC removes its own unnecessary intrasite connections.
470 This function tags them with the to_be_deleted attribute.
474 # XXX should an RODC be regarded as same site? It isn't part
475 # of the intrasite ring.
477 if self.my_site.is_cleanup_ntdsconn_disabled():
478 DEBUG_FN("not doing ntdsconn cleanup for site %s, "
479 "because it is disabled" % self.my_site)
485 self._ensure_connections_are_loaded(mydsa.connect_table.values())
487 # RODC never actually added any connections to begin with
491 local_connections = []
493 for cn_conn in mydsa.connect_table.values():
494 s_dnstr = cn_conn.get_from_dnstr()
495 if s_dnstr in self.my_site.dsa_table:
496 removable = not (cn_conn.is_generated() or
497 cn_conn.is_rodc_topology())
498 packed_guid = ndr_pack(cn_conn.guid)
499 local_connections.append((cn_conn, s_dnstr,
500 packed_guid, removable))
502 for a, b in itertools.permutations(local_connections, 2):
503 cn_conn, s_dnstr, packed_guid, removable = a
504 cn_conn2, s_dnstr2, packed_guid2, removable2 = b
506 s_dnstr == s_dnstr2 and
507 cn_conn.whenCreated < cn_conn2.whenCreated or
508 (cn_conn.whenCreated == cn_conn2.whenCreated and
509 packed_guid < packed_guid2)):
510 cn_conn.to_be_deleted = True
512 def _mark_unneeded_intersite_ntdsconn(self):
513 """find unneeded intersite NTDS Connections for removal
515 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections. The
516 intersite topology generator removes links for all DCs in its
517 site. Here we just tag them with the to_be_deleted attribute.
521 # TODO Figure out how best to handle the RODC case
522 # The RODC is ISTG, but shouldn't act on anyone's behalf.
523 if self.my_dsa.is_ro():
526 # Find the intersite connections
527 local_dsas = self.my_site.dsa_table
528 connections_and_dsas = []
529 for dsa in local_dsas.values():
530 for cn in dsa.connect_table.values():
533 s_dnstr = cn.get_from_dnstr()
536 if s_dnstr not in local_dsas:
537 from_dsa = self.get_dsa(s_dnstr)
538 # Samba ONLY: ISTG removes connections to dead DCs
539 if from_dsa is None or '\\0ADEL' in s_dnstr:
540 logger.info("DSA appears deleted, removing connection %s"
542 cn.to_be_deleted = True
544 connections_and_dsas.append((cn, dsa, from_dsa))
546 self._ensure_connections_are_loaded(x[0] for x in connections_and_dsas)
547 for cn, to_dsa, from_dsa in connections_and_dsas:
548 if not cn.is_generated() or cn.is_rodc_topology():
551 # If the connection is in the kept_connections list, we
552 # only remove it if an endpoint seems down.
553 if (cn in self.kept_connections and
554 not (self.is_bridgehead_failed(to_dsa, True) or
555 self.is_bridgehead_failed(from_dsa, True))):
558 # this one is broken and might be superseded by another.
559 # But which other? Let's just say another link to the same
560 # site can supersede.
561 from_dnstr = from_dsa.dsa_dnstr
562 for site in self.site_table.values():
563 if from_dnstr in site.rw_dsa_table:
564 for cn2, to_dsa2, from_dsa2 in connections_and_dsas:
565 if (cn is not cn2 and
566 from_dsa2 in site.rw_dsa_table):
567 cn.to_be_deleted = True
569 def _commit_changes(self, dsa):
570 if dsa.is_ro() or self.readonly:
571 for connect in dsa.connect_table.values():
572 if connect.to_be_deleted:
573 logger.info("TO BE DELETED:\n%s" % connect)
574 if connect.to_be_added:
575 logger.info("TO BE ADDED:\n%s" % connect)
576 if connect.to_be_modified:
577 logger.info("TO BE MODIFIED:\n%s" % connect)
579 # Peform deletion from our tables but perform
580 # no database modification
581 dsa.commit_connections(self.samdb, ro=True)
583 # Commit any modified connections
584 dsa.commit_connections(self.samdb)
586 def remove_unneeded_ntdsconn(self, all_connected):
587 """Remove unneeded NTDS Connections once topology is calculated
589 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections
591 :param all_connected: indicates whether all sites are connected
594 self._mark_broken_ntdsconn()
595 self._mark_unneeded_local_ntdsconn()
596 # if we are not the istg, we're done!
597 # if we are the istg, but all_connected is False, we also do nothing.
598 if self.my_dsa.is_istg() and all_connected:
599 self._mark_unneeded_intersite_ntdsconn()
601 for dsa in self.my_site.dsa_table.values():
602 self._commit_changes(dsa)
604 def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
605 """Update an repsFrom object if required.
607 Part of MS-ADTS 6.2.2.5.
609 Update t_repsFrom if necessary to satisfy requirements. Such
610 updates are typically required when the IDL_DRSGetNCChanges
611 server has moved from one site to another--for example, to
612 enable compression when the server is moved from the
613 client's site to another site.
615 The repsFrom.update_flags bit field may be modified
616 auto-magically if any changes are made here. See
617 kcc_utils.RepsFromTo for gory details.
620 :param n_rep: NC replica we need
621 :param t_repsFrom: repsFrom tuple to modify
622 :param s_rep: NC replica at source DSA
623 :param s_dsa: source DSA
624 :param cn_conn: Local DSA NTDSConnection child
628 s_dnstr = s_dsa.dsa_dnstr
629 same_site = s_dnstr in self.my_site.dsa_table
631 # if schedule doesn't match then update and modify
632 times = convert_schedule_to_repltimes(cn_conn.schedule)
633 if times != t_repsFrom.schedule:
634 t_repsFrom.schedule = times
636 # Bit DRS_ADD_REF is set in replicaFlags unconditionally
638 if ((t_repsFrom.replica_flags &
639 drsuapi.DRSUAPI_DRS_ADD_REF) == 0x0):
640 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_ADD_REF
642 # Bit DRS_PER_SYNC is set in replicaFlags if and only
643 # if nTDSConnection schedule has a value v that specifies
644 # scheduled replication is to be performed at least once
646 if cn_conn.is_schedule_minimum_once_per_week():
648 if ((t_repsFrom.replica_flags &
649 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0):
650 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
652 # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
653 # if the source DSA and the local DC's nTDSDSA object are
654 # in the same site or source dsa is the FSMO role owner
655 # of one or more FSMO roles in the NC replica.
656 if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
658 if ((t_repsFrom.replica_flags &
659 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0):
660 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
662 # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
663 # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
664 # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
665 # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
666 # t.replicaFlags if and only if s and the local DC's
667 # nTDSDSA object are in different sites.
668 if ((cn_conn.options &
669 dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0):
671 if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
674 # it LOOKS as if this next test is a bit silly: it
675 # checks the flag then sets it if it not set; the same
676 # effect could be achieved by unconditionally setting
677 # it. But in fact the repsFrom object has special
678 # magic attached to it, and altering replica_flags has
679 # side-effects. That is bad in my opinion, but there
681 if ((t_repsFrom.replica_flags &
682 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
683 t_repsFrom.replica_flags |= \
684 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
688 if ((t_repsFrom.replica_flags &
689 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
690 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
692 # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
693 # and only if s and the local DC's nTDSDSA object are
694 # not in the same site and the
695 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
696 # clear in cn!options
697 if (not same_site and
699 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
701 if ((t_repsFrom.replica_flags &
702 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0):
703 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
705 # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
706 # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
707 if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
709 if ((t_repsFrom.replica_flags &
710 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0):
711 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
713 # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
714 # set in t.replicaFlags if and only if cn!enabledConnection = false.
715 if not cn_conn.is_enabled():
717 if ((t_repsFrom.replica_flags &
718 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0):
719 t_repsFrom.replica_flags |= \
720 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
722 if ((t_repsFrom.replica_flags &
723 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0):
724 t_repsFrom.replica_flags |= \
725 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
727 # If s and the local DC's nTDSDSA object are in the same site,
728 # cn!transportType has no value, or the RDN of cn!transportType
731 # Bit DRS_MAIL_REP in t.replicaFlags is clear.
733 # t.uuidTransport = NULL GUID.
735 # t.uuidDsa = The GUID-based DNS name of s.
739 # Bit DRS_MAIL_REP in t.replicaFlags is set.
741 # If x is the object with dsname cn!transportType,
742 # t.uuidTransport = x!objectGUID.
744 # Let a be the attribute identified by
745 # x!transportAddressAttribute. If a is
746 # the dNSHostName attribute, t.uuidDsa = the GUID-based
747 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
749 # It appears that the first statement i.e.
751 # "If s and the local DC's nTDSDSA object are in the same
752 # site, cn!transportType has no value, or the RDN of
753 # cn!transportType is CN=IP:"
755 # could be a slightly tighter statement if it had an "or"
756 # between each condition. I believe this should
759 # IF (same-site) OR (no-value) OR (type-ip)
761 # because IP should be the primary transport mechanism
762 # (even in inter-site) and the absense of the transportType
763 # attribute should always imply IP no matter if its multi-site
765 # NOTE MS-TECH INCORRECT:
767 # All indications point to these statements above being
768 # incorrectly stated:
770 # t.uuidDsa = The GUID-based DNS name of s.
772 # Let a be the attribute identified by
773 # x!transportAddressAttribute. If a is
774 # the dNSHostName attribute, t.uuidDsa = the GUID-based
775 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
777 # because the uuidDSA is a GUID and not a GUID-base DNS
778 # name. Nor can uuidDsa hold (s!parent)!a if not
779 # dNSHostName. What should have been said is:
781 # t.naDsa = The GUID-based DNS name of s
783 # That would also be correct if transportAddressAttribute
784 # were "mailAddress" because (naDsa) can also correctly
785 # hold the SMTP ISM service address.
787 nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
789 if ((t_repsFrom.replica_flags &
790 drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0):
791 t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
793 t_repsFrom.transport_guid = misc.GUID()
795 # See (NOTE MS-TECH INCORRECT) above
797 # NOTE: it looks like these conditionals are pointless,
798 # because the state will end up as `t_repsFrom.dns_name1 ==
799 # nastr` in either case, BUT the repsFrom thing is magic and
800 # assigning to it alters some flags. So we try not to update
801 # it unless necessary.
802 if t_repsFrom.dns_name1 != nastr:
803 t_repsFrom.dns_name1 = nastr
805 if t_repsFrom.version > 0x1 and t_repsFrom.dns_name2 != nastr:
806 t_repsFrom.dns_name2 = nastr
808 if t_repsFrom.is_modified():
809 DEBUG_FN("modify_repsFrom(): %s" % t_repsFrom)
811 def get_dsa_for_implied_replica(self, n_rep, cn_conn):
812 """If a connection imply a replica, find the relevant DSA
814 Given a NC replica and NTDS Connection, determine if the
815 connection implies a repsFrom tuple should be present from the
816 source DSA listed in the connection to the naming context. If
817 it should be, return the DSA; otherwise return None.
819 Based on part of MS-ADTS 6.2.2.5
821 :param n_rep: NC replica
822 :param cn_conn: NTDS Connection
823 :return: source DSA or None
825 # XXX different conditions for "implies" than MS-ADTS 6.2.2
828 # It boils down to: we want an enabled, non-FRS connections to
829 # a valid remote DSA with a non-RO replica corresponding to
832 if not cn_conn.is_enabled() or cn_conn.is_rodc_topology():
835 s_dnstr = cn_conn.get_from_dnstr()
836 s_dsa = self.get_dsa(s_dnstr)
838 # No DSA matching this source DN string?
842 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
844 if (s_rep is not None and
845 s_rep.is_present() and
846 (not s_rep.is_ro() or n_rep.is_partial())):
850 def translate_ntdsconn(self, current_dsa=None):
851 """Adjust repsFrom to match NTDSConnections
853 This function adjusts values of repsFrom abstract attributes of NC
854 replicas on the local DC to match those implied by
855 nTDSConnection objects.
857 Based on [MS-ADTS] 6.2.2.5
859 :param current_dsa: optional DSA on whose behalf we are acting.
863 if current_dsa is None:
864 current_dsa = self.my_dsa
866 if current_dsa.is_ro():
869 if current_dsa.is_translate_ntdsconn_disabled():
870 DEBUG_FN("skipping translate_ntdsconn() "
871 "because disabling flag is set")
874 DEBUG_FN("translate_ntdsconn(): enter")
876 current_rep_table, needed_rep_table = current_dsa.get_rep_tables()
878 # Filled in with replicas we currently have that need deleting
881 # We're using the MS notation names here to allow
882 # correlation back to the published algorithm.
884 # n_rep - NC replica (n)
885 # t_repsFrom - tuple (t) in n!repsFrom
886 # s_dsa - Source DSA of the replica. Defined as nTDSDSA
887 # object (s) such that (s!objectGUID = t.uuidDsa)
888 # In our IDL representation of repsFrom the (uuidDsa)
889 # attribute is called (source_dsa_obj_guid)
890 # cn_conn - (cn) is nTDSConnection object and child of the local
891 # DC's nTDSDSA object and (cn!fromServer = s)
892 # s_rep - source DSA replica of n
894 # If we have the replica and its not needed
895 # then we add it to the "to be deleted" list.
896 for dnstr in current_rep_table:
897 # If we're on the RODC, hardcode the update flags
899 c_rep = current_rep_table[dnstr]
900 c_rep.load_repsFrom(self.samdb)
901 for t_repsFrom in c_rep.rep_repsFrom:
902 replica_flags = (drsuapi.DRSUAPI_DRS_INIT_SYNC |
903 drsuapi.DRSUAPI_DRS_PER_SYNC |
904 drsuapi.DRSUAPI_DRS_ADD_REF |
905 drsuapi.DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING |
906 drsuapi.DRSUAPI_DRS_NONGC_RO_REP)
907 if t_repsFrom.replica_flags != replica_flags:
908 t_repsFrom.replica_flags = replica_flags
909 c_rep.commit_repsFrom(self.samdb, ro=self.readonly)
911 if dnstr not in needed_rep_table:
912 delete_reps.add(dnstr)
914 DEBUG_FN('current %d needed %d delete %d' % (len(current_rep_table),
915 len(needed_rep_table), len(delete_reps)))
918 # TODO Must delete repsFrom/repsTo for these replicas
919 DEBUG('deleting these reps: %s' % delete_reps)
920 for dnstr in delete_reps:
921 del current_rep_table[dnstr]
925 # Now perform the scan of replicas we'll need
926 # and compare any current repsFrom against the
928 for n_rep in needed_rep_table.values():
930 # load any repsFrom and fsmo roles as we'll
931 # need them during connection translation
932 n_rep.load_repsFrom(self.samdb)
933 n_rep.load_fsmo_roles(self.samdb)
935 # Loop thru the existing repsFrom tuples (if any)
936 # XXX This is a list and could contain duplicates
937 # (multiple load_repsFrom calls)
938 for t_repsFrom in n_rep.rep_repsFrom:
940 # for each tuple t in n!repsFrom, let s be the nTDSDSA
941 # object such that s!objectGUID = t.uuidDsa
942 guidstr = str(t_repsFrom.source_dsa_obj_guid)
943 s_dsa = self.get_dsa_by_guidstr(guidstr)
945 # Source dsa is gone from config (strange)
946 # so cleanup stale repsFrom for unlisted DSA
948 logger.warning("repsFrom source DSA guid (%s) not found" %
950 t_repsFrom.to_be_deleted = True
953 # Find the connection that this repsFrom would use. If
954 # there isn't a good one (i.e. non-RODC_TOPOLOGY,
955 # meaning non-FRS), we delete the repsFrom.
956 s_dnstr = s_dsa.dsa_dnstr
957 connections = current_dsa.get_connection_by_from_dnstr(s_dnstr)
958 for cn_conn in connections:
959 if not cn_conn.is_rodc_topology():
962 # no break means no non-rodc_topology connection exists
963 t_repsFrom.to_be_deleted = True
966 # KCC removes this repsFrom tuple if any of the following
968 # No NC replica of the NC "is present" on DSA that
969 # would be source of replica
971 # A writable replica of the NC "should be present" on
972 # the local DC, but a partial replica "is present" on
974 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
976 if s_rep is None or not s_rep.is_present() or \
977 (not n_rep.is_ro() and s_rep.is_partial()):
979 t_repsFrom.to_be_deleted = True
982 # If the KCC did not remove t from n!repsFrom, it updates t
983 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
985 # Loop thru connections and add implied repsFrom tuples
986 # for each NTDSConnection under our local DSA if the
987 # repsFrom is not already present
988 for cn_conn in current_dsa.connect_table.values():
990 s_dsa = self.get_dsa_for_implied_replica(n_rep, cn_conn)
994 # Loop thru the existing repsFrom tuples (if any) and
995 # if we already have a tuple for this connection then
996 # no need to proceed to add. It will have been changed
997 # to have the correct attributes above
998 for t_repsFrom in n_rep.rep_repsFrom:
999 guidstr = str(t_repsFrom.source_dsa_obj_guid)
1000 if s_dsa is self.get_dsa_by_guidstr(guidstr):
1007 # Create a new RepsFromTo and proceed to modify
1008 # it according to specification
1009 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
1011 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
1013 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
1015 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
1017 # Add to our NC repsFrom as this is newly computed
1018 if t_repsFrom.is_modified():
1019 n_rep.rep_repsFrom.append(t_repsFrom)
1021 if self.readonly or ro:
1022 # Display any to be deleted or modified repsFrom
1023 text = n_rep.dumpstr_to_be_deleted()
1025 logger.info("TO BE DELETED:\n%s" % text)
1026 text = n_rep.dumpstr_to_be_modified()
1028 logger.info("TO BE MODIFIED:\n%s" % text)
1030 # Peform deletion from our tables but perform
1031 # no database modification
1032 n_rep.commit_repsFrom(self.samdb, ro=True)
1034 # Commit any modified repsFrom to the NC replica
1035 n_rep.commit_repsFrom(self.samdb)
1039 # Now perform the scan of replicas we'll need
1040 # and compare any current repsTo against the
1043 # RODC should never push to anybody (should we check this?)
1047 for n_rep in needed_rep_table.values():
1049 # load any repsTo and fsmo roles as we'll
1050 # need them during connection translation
1051 n_rep.load_repsTo(self.samdb)
1053 # Loop thru the existing repsTo tuples (if any)
1054 # XXX This is a list and could contain duplicates
1055 # (multiple load_repsTo calls)
1056 for t_repsTo in n_rep.rep_repsTo:
1058 # for each tuple t in n!repsTo, let s be the nTDSDSA
1059 # object such that s!objectGUID = t.uuidDsa
1060 guidstr = str(t_repsTo.source_dsa_obj_guid)
1061 s_dsa = self.get_dsa_by_guidstr(guidstr)
1063 # Source dsa is gone from config (strange)
1064 # so cleanup stale repsTo for unlisted DSA
1066 logger.warning("repsTo source DSA guid (%s) not found" %
1068 t_repsTo.to_be_deleted = True
1071 # Find the connection that this repsTo would use. If
1072 # there isn't a good one (i.e. non-RODC_TOPOLOGY,
1073 # meaning non-FRS), we delete the repsTo.
1074 s_dnstr = s_dsa.dsa_dnstr
1075 if '\\0ADEL' in s_dnstr:
1076 logger.warning("repsTo source DSA guid (%s) appears deleted" %
1078 t_repsTo.to_be_deleted = True
1081 connections = s_dsa.get_connection_by_from_dnstr(self.my_dsa_dnstr)
1082 if len(connections) > 0:
1083 # Then this repsTo is tentatively valid
1086 # There is no plausible connection for this repsTo
1087 t_repsTo.to_be_deleted = True
1090 # Display any to be deleted or modified repsTo
1091 for rt in n_rep.rep_repsTo:
1092 if rt.to_be_deleted:
1093 logger.info("REMOVING REPS-TO: %s" % rt)
1095 # Peform deletion from our tables but perform
1096 # no database modification
1097 n_rep.commit_repsTo(self.samdb, ro=True)
1099 # Commit any modified repsTo to the NC replica
1100 n_rep.commit_repsTo(self.samdb)
1102 # TODO Remove any duplicate repsTo values. This should never happen in
1103 # any normal situations.
1105 def merge_failed_links(self, ping=None):
1106 """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
1108 The KCC on a writable DC attempts to merge the link and connection
1109 failure information from bridgehead DCs in its own site to help it
1110 identify failed bridgehead DCs.
1112 Based on MS-ADTS 6.2.2.3.2 "Merge of kCCFailedLinks and kCCFailedLinks
1115 :param ping: An oracle of current bridgehead availability
1118 # 1. Queries every bridgehead server in your site (other than yourself)
1119 # 2. For every ntDSConnection that references a server in a different
1120 # site merge all the failure info
1122 # XXX - not implemented yet
1123 if ping is not None:
1124 debug.DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1126 DEBUG_FN("skipping merge_failed_links() because it requires "
1127 "real network connections\n"
1128 "and we weren't asked to --attempt-live-connections")
1130 def setup_graph(self, part):
1131 """Set up an intersite graph
1133 An intersite graph has a Vertex for each site object, a
1134 MultiEdge for each SiteLink object, and a MutliEdgeSet for
1135 each siteLinkBridge object (or implied siteLinkBridge). It
1136 reflects the intersite topology in a slightly more abstract
1139 Roughly corresponds to MS-ADTS 6.2.2.3.4.3
1141 :param part: a Partition object
1142 :returns: an InterSiteGraph object
1144 # If 'Bridge all site links' is enabled and Win2k3 bridges required
1146 # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1147 # No documentation for this however, ntdsapi.h appears to have:
1148 # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1149 bridges_required = self.my_site.site_options & 0x00001002 != 0
1150 transport_guid = str(self.ip_transport.guid)
1152 g = setup_graph(part, self.site_table, transport_guid,
1153 self.sitelink_table, bridges_required)
1155 if self.verify or self.dot_file_dir is not None:
1157 for edge in g.edges:
1158 for a, b in itertools.combinations(edge.vertices, 2):
1159 dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1160 verify_properties = ()
1161 name = 'site_edges_%s' % part.partstr
1162 verify_and_dot(name, dot_edges, directed=False,
1163 label=self.my_dsa_dnstr,
1164 properties=verify_properties, debug=DEBUG,
1166 dot_file_dir=self.dot_file_dir)
1170 def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1171 """Get a bridghead DC for a site.
1173 Part of MS-ADTS 6.2.2.3.4.4
1175 :param site: site object representing for which a bridgehead
1177 :param part: crossRef for NC to replicate.
1178 :param transport: interSiteTransport object for replication
1180 :param partial_ok: True if a DC containing a partial
1181 replica or a full replica will suffice, False if only
1182 a full replica will suffice.
1183 :param detect_failed: True to detect failed DCs and route
1184 replication traffic around them, False to assume no DC
1186 :return: dsa object for the bridgehead DC or None
1189 bhs = self.get_all_bridgeheads(site, part, transport,
1190 partial_ok, detect_failed)
1192 debug.DEBUG_MAGENTA("get_bridgehead FAILED:\nsitedn = %s" %
1196 debug.DEBUG_GREEN("get_bridgehead:\n\tsitedn = %s\n\tbhdn = %s" %
1197 (site.site_dnstr, bhs[0].dsa_dnstr))
1200 def get_all_bridgeheads(self, site, part, transport,
1201 partial_ok, detect_failed):
1202 """Get all bridghead DCs on a site satisfying the given criteria
1204 Part of MS-ADTS 6.2.2.3.4.4
1206 :param site: site object representing the site for which
1207 bridgehead DCs are desired.
1208 :param part: partition for NC to replicate.
1209 :param transport: interSiteTransport object for
1210 replication traffic.
1211 :param partial_ok: True if a DC containing a partial
1212 replica or a full replica will suffice, False if
1213 only a full replica will suffice.
1214 :param detect_failed: True to detect failed DCs and route
1215 replication traffic around them, FALSE to assume
1217 :return: list of dsa object for available bridgehead DCs
1221 if transport.name != "IP":
1222 raise KCCError("get_all_bridgeheads has run into a "
1223 "non-IP transport! %r"
1224 % (transport.name,))
1226 DEBUG_FN(site.rw_dsa_table)
1227 for dsa in site.rw_dsa_table.values():
1229 pdnstr = dsa.get_parent_dnstr()
1231 # IF t!bridgeheadServerListBL has one or more values and
1232 # t!bridgeheadServerListBL does not contain a reference
1233 # to the parent object of dc then skip dc
1234 if ((len(transport.bridgehead_list) != 0 and
1235 pdnstr not in transport.bridgehead_list)):
1238 # IF dc is in the same site as the local DC
1239 # IF a replica of cr!nCName is not in the set of NC replicas
1240 # that "should be present" on dc or a partial replica of the
1241 # NC "should be present" but partialReplicasOkay = FALSE
1243 if self.my_site.same_site(dsa):
1244 needed, ro, partial = part.should_be_present(dsa)
1245 if not needed or (partial and not partial_ok):
1247 rep = dsa.get_current_replica(part.nc_dnstr)
1250 # IF an NC replica of cr!nCName is not in the set of NC
1251 # replicas that "are present" on dc or a partial replica of
1252 # the NC "is present" but partialReplicasOkay = FALSE
1255 rep = dsa.get_current_replica(part.nc_dnstr)
1256 if rep is None or (rep.is_partial() and not partial_ok):
1259 # IF AmIRODC() and cr!nCName corresponds to default NC then
1260 # Let dsaobj be the nTDSDSA object of the dc
1261 # IF dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1263 if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1264 if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1267 # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1269 if self.is_bridgehead_failed(dsa, detect_failed):
1270 DEBUG("bridgehead is failed")
1273 DEBUG_FN("found a bridgehead: %s" % dsa.dsa_dnstr)
1276 # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1278 # SORT bhs such that all GC servers precede DCs that are not GC
1279 # servers, and otherwise by ascending objectGUID
1281 # SORT bhs in a random order
1282 if site.is_random_bridgehead_disabled():
1283 bhs.sort(sort_dsa_by_gc_and_guid)
1286 debug.DEBUG_YELLOW(bhs)
1289 def is_bridgehead_failed(self, dsa, detect_failed):
1290 """Determine whether a given DC is known to be in a failed state
1292 :param dsa: the bridgehead to test
1293 :param detect_failed: True to really check, False to assume no failure
1294 :return: True if and only if the DC should be considered failed
1296 Here we DEPART from the pseudo code spec which appears to be
1297 wrong. It says, in full:
1299 /***** BridgeheadDCFailed *****/
1300 /* Determine whether a given DC is known to be in a failed state.
1301 * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1302 * IN: detectFailedDCs - TRUE if and only failed DC detection is
1304 * RETURNS: TRUE if and only if the DC should be considered to be in a
1307 BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1309 IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1310 the options attribute of the site settings object for the local
1313 ELSEIF a tuple z exists in the kCCFailedLinks or
1314 kCCFailedConnections variables such that z.UUIDDsa =
1315 objectGUID, z.FailureCount > 1, and the current time -
1316 z.TimeFirstFailure > 2 hours
1319 RETURN detectFailedDCs
1323 where you will see detectFailedDCs is not behaving as
1324 advertised -- it is acting as a default return code in the
1325 event that a failure is not detected, not a switch turning
1326 detection on or off. Elsewhere the documentation seems to
1327 concur with the comment rather than the code.
1329 if not detect_failed:
1332 # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1333 # When DETECT_STALE_DISABLED, we can never know of if
1334 # it's in a failed state
1335 if self.my_site.site_options & 0x00000008:
1338 return self.is_stale_link_connection(dsa)
1340 def create_connection(self, part, rbh, rsite, transport,
1341 lbh, lsite, link_opt, link_sched,
1342 partial_ok, detect_failed):
1343 """Create an nTDSConnection object as specified if it doesn't exist.
1345 Part of MS-ADTS 6.2.2.3.4.5
1347 :param part: crossRef object for the NC to replicate.
1348 :param rbh: nTDSDSA object for DC to act as the
1349 IDL_DRSGetNCChanges server (which is in a site other
1350 than the local DC's site).
1351 :param rsite: site of the rbh
1352 :param transport: interSiteTransport object for the transport
1353 to use for replication traffic.
1354 :param lbh: nTDSDSA object for DC to act as the
1355 IDL_DRSGetNCChanges client (which is in the local DC's site).
1356 :param lsite: site of the lbh
1357 :param link_opt: Replication parameters (aggregated siteLink options,
1359 :param link_sched: Schedule specifying the times at which
1360 to begin replicating.
1361 :partial_ok: True if bridgehead DCs containing partial
1362 replicas of the NC are acceptable.
1363 :param detect_failed: True to detect failed DCs and route
1364 replication traffic around them, FALSE to assume no DC
1367 rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1369 rbh_table = dict((x.dsa_dnstr, x) for x in rbhs_all)
1371 debug.DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all),
1372 [x.dsa_dnstr for x in rbhs_all]))
1374 # MS-TECH says to compute rbhs_avail but then doesn't use it
1375 # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1376 # partial_ok, detect_failed)
1378 lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1381 lbhs_all.append(lbh)
1383 debug.DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all),
1384 [x.dsa_dnstr for x in lbhs_all]))
1386 # MS-TECH says to compute lbhs_avail but then doesn't use it
1387 # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1388 # partial_ok, detect_failed)
1390 # FOR each nTDSConnection object cn such that the parent of cn is
1391 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1392 for ldsa in lbhs_all:
1393 for cn in ldsa.connect_table.values():
1395 rdsa = rbh_table.get(cn.from_dnstr)
1399 debug.DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1400 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1401 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1402 # cn!transportType references t
1403 if ((cn.is_generated() and
1404 not cn.is_rodc_topology() and
1405 cn.transport_guid == transport.guid)):
1407 # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1408 # cn!options and cn!schedule != sch
1409 # Perform an originating update to set cn!schedule to
1411 if ((not cn.is_user_owned_schedule() and
1412 not cn.is_equivalent_schedule(link_sched))):
1413 cn.schedule = link_sched
1414 cn.set_modified(True)
1416 # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1417 # NTDSCONN_OPT_USE_NOTIFY are set in cn
1418 if cn.is_override_notify_default() and \
1421 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1423 # Perform an originating update to clear bits
1424 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1425 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1426 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1428 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1429 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1430 cn.set_modified(True)
1435 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1437 # Perform an originating update to set bits
1438 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1439 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1440 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1442 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1443 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1444 cn.set_modified(True)
1446 # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1447 if cn.is_twoway_sync():
1449 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1451 # Perform an originating update to clear bit
1452 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1453 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1454 cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1455 cn.set_modified(True)
1460 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1462 # Perform an originating update to set bit
1463 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1464 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1465 cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1466 cn.set_modified(True)
1468 # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1470 if cn.is_intersite_compression_disabled():
1472 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1474 # Perform an originating update to clear bit
1475 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1478 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0):
1480 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1481 cn.set_modified(True)
1485 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1487 # Perform an originating update to set bit
1488 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1491 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1493 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1494 cn.set_modified(True)
1496 # Display any modified connection
1497 if self.readonly or ldsa.is_ro():
1498 if cn.to_be_modified:
1499 logger.info("TO BE MODIFIED:\n%s" % cn)
1501 ldsa.commit_connections(self.samdb, ro=True)
1503 ldsa.commit_connections(self.samdb)
1506 valid_connections = 0
1508 # FOR each nTDSConnection object cn such that cn!parent is
1509 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1510 for ldsa in lbhs_all:
1511 for cn in ldsa.connect_table.values():
1513 rdsa = rbh_table.get(cn.from_dnstr)
1517 debug.DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1519 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1520 # cn!transportType references t) and
1521 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1522 if (((not cn.is_generated() or
1523 cn.transport_guid == transport.guid) and
1524 not cn.is_rodc_topology())):
1526 # LET rguid be the objectGUID of the nTDSDSA object
1527 # referenced by cn!fromServer
1528 # LET lguid be (cn!parent)!objectGUID
1530 # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1531 # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1532 # Increment cValidConnections by 1
1533 if ((not self.is_bridgehead_failed(rdsa, detect_failed) and
1534 not self.is_bridgehead_failed(ldsa, detect_failed))):
1535 valid_connections += 1
1537 # IF keepConnections does not contain cn!objectGUID
1538 # APPEND cn!objectGUID to keepConnections
1539 self.kept_connections.add(cn)
1542 debug.DEBUG_RED("valid connections %d" % valid_connections)
1543 DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1544 # IF cValidConnections = 0
1545 if valid_connections == 0:
1547 # LET opt be NTDSCONN_OPT_IS_GENERATED
1548 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1550 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1551 # SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1552 # NTDSCONN_OPT_USE_NOTIFY in opt
1553 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1554 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1555 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1557 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1558 # SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1559 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1560 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1562 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1564 # SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1566 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1567 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1569 # Perform an originating update to create a new nTDSConnection
1570 # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1571 # cn!options = opt, cn!transportType is a reference to t,
1572 # cn!fromServer is a reference to rbh, and cn!schedule = sch
1573 DEBUG_FN("new connection, KCC dsa: %s" % self.my_dsa.dsa_dnstr)
1574 system_flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1575 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1577 cn = lbh.new_connection(opt, system_flags, transport,
1578 rbh.dsa_dnstr, link_sched)
1580 # Display any added connection
1581 if self.readonly or lbh.is_ro():
1583 logger.info("TO BE ADDED:\n%s" % cn)
1585 lbh.commit_connections(self.samdb, ro=True)
1587 lbh.commit_connections(self.samdb)
1589 # APPEND cn!objectGUID to keepConnections
1590 self.kept_connections.add(cn)
1592 def add_transports(self, vertex, local_vertex, graph, detect_failed):
1593 """Build a Vertex's transport lists
1595 Each vertex has accept_red_red and accept_black lists that
1596 list what transports they accept under various conditions. The
1597 only transport that is ever accepted is IP, and a dummy extra
1598 transport called "EDGE_TYPE_ALL".
1600 Part of MS-ADTS 6.2.2.3.4.3 -- ColorVertices
1602 :param vertex: the remote vertex we are thinking about
1603 :param local_vertex: the vertex relating to the local site.
1604 :param graph: the intersite graph
1605 :param detect_failed: whether to detect failed links
1606 :return: True if some bridgeheads were not found
1608 # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1609 # here, but using vertex seems to make more sense. That is,
1610 # the docs want this:
1612 #bh = self.get_bridgehead(local_vertex.site, vertex.part, transport,
1613 # local_vertex.is_black(), detect_failed)
1617 vertex.accept_red_red = []
1618 vertex.accept_black = []
1619 found_failed = False
1621 if vertex in graph.connected_vertices:
1622 t_guid = str(self.ip_transport.guid)
1624 bh = self.get_bridgehead(vertex.site, vertex.part,
1626 vertex.is_black(), detect_failed)
1628 if vertex.site.is_rodc_site():
1629 vertex.accept_red_red.append(t_guid)
1633 vertex.accept_red_red.append(t_guid)
1634 vertex.accept_black.append(t_guid)
1636 # Add additional transport to ensure another run of Dijkstra
1637 vertex.accept_red_red.append("EDGE_TYPE_ALL")
1638 vertex.accept_black.append("EDGE_TYPE_ALL")
1642 def create_connections(self, graph, part, detect_failed):
1643 """Create intersite NTDSConnections as needed by a partition
1645 Construct an NC replica graph for the NC identified by
1646 the given crossRef, then create any additional nTDSConnection
1649 :param graph: site graph.
1650 :param part: crossRef object for NC.
1651 :param detect_failed: True to detect failed DCs and route
1652 replication traffic around them, False to assume no DC
1655 Modifies self.kept_connections by adding any connections
1656 deemed to be "in use".
1658 :return: (all_connected, found_failed_dc)
1659 (all_connected) True if the resulting NC replica graph
1660 connects all sites that need to be connected.
1661 (found_failed_dc) True if one or more failed DCs were
1664 all_connected = True
1665 found_failed = False
1667 DEBUG_FN("create_connections(): enter\n"
1668 "\tpartdn=%s\n\tdetect_failed=%s" %
1669 (part.nc_dnstr, detect_failed))
1671 # XXX - This is a highly abbreviated function from the MS-TECH
1672 # ref. It creates connections between bridgeheads to all
1673 # sites that have appropriate replicas. Thus we are not
1674 # creating a minimum cost spanning tree but instead
1675 # producing a fully connected tree. This should produce
1676 # a full (albeit not optimal cost) replication topology.
1678 my_vertex = Vertex(self.my_site, part)
1679 my_vertex.color_vertex()
1681 for v in graph.vertices:
1683 if self.add_transports(v, my_vertex, graph, detect_failed):
1686 # No NC replicas for this NC in the site of the local DC,
1687 # so no nTDSConnection objects need be created
1688 if my_vertex.is_white():
1689 return all_connected, found_failed
1691 edge_list, n_components = get_spanning_tree_edges(graph,
1695 DEBUG_FN("%s Number of components: %d" %
1696 (part.nc_dnstr, n_components))
1697 if n_components > 1:
1698 all_connected = False
1700 # LET partialReplicaOkay be TRUE if and only if
1701 # localSiteVertex.Color = COLOR.BLACK
1702 partial_ok = my_vertex.is_black()
1704 # Utilize the IP transport only for now
1705 transport = self.ip_transport
1707 DEBUG("edge_list %s" % edge_list)
1709 # XXX more accurate comparison?
1710 if e.directed and e.vertices[0].site is self.my_site:
1713 if e.vertices[0].site is self.my_site:
1714 rsite = e.vertices[1].site
1716 rsite = e.vertices[0].site
1718 # We don't make connections to our own site as that
1719 # is intrasite topology generator's job
1720 if rsite is self.my_site:
1721 DEBUG("rsite is my_site")
1724 # Determine bridgehead server in remote site
1725 rbh = self.get_bridgehead(rsite, part, transport,
1726 partial_ok, detect_failed)
1730 # RODC acts as an BH for itself
1732 # LET lbh be the nTDSDSA object of the local DC
1734 # LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1735 # cr, t, partialReplicaOkay, detectFailedDCs)
1736 if self.my_dsa.is_ro():
1737 lsite = self.my_site
1740 lsite = self.my_site
1741 lbh = self.get_bridgehead(lsite, part, transport,
1742 partial_ok, detect_failed)
1745 debug.DEBUG_RED("DISASTER! lbh is None")
1748 DEBUG_FN("lsite: %s\nrsite: %s" % (lsite, rsite))
1749 DEBUG_FN("vertices %s" % (e.vertices,))
1750 debug.DEBUG_BLUE("bridgeheads\n%s\n%s\n%s" % (lbh, rbh, "-" * 70))
1752 sitelink = e.site_link
1753 if sitelink is None:
1757 link_opt = sitelink.options
1758 link_sched = sitelink.schedule
1760 self.create_connection(part, rbh, rsite, transport,
1761 lbh, lsite, link_opt, link_sched,
1762 partial_ok, detect_failed)
1764 return all_connected, found_failed
1766 def create_intersite_connections(self):
1767 """Create NTDSConnections as necessary for all partitions.
1769 Computes an NC replica graph for each NC replica that "should be
1770 present" on the local DC or "is present" on any DC in the same site
1771 as the local DC. For each edge directed to an NC replica on such a
1772 DC from an NC replica on a DC in another site, the KCC creates an
1773 nTDSConnection object to imply that edge if one does not already
1776 Modifies self.kept_connections - A set of nTDSConnection
1777 objects for edges that are directed
1778 to the local DC's site in one or more NC replica graphs.
1780 :return: True if spanning trees were created for all NC replica
1781 graphs, otherwise False.
1783 all_connected = True
1784 self.kept_connections = set()
1786 # LET crossRefList be the set containing each object o of class
1787 # crossRef such that o is a child of the CN=Partitions child of the
1790 # FOR each crossRef object cr in crossRefList
1791 # IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1792 # is clear in cr!systemFlags, skip cr.
1793 # LET g be the GRAPH return of SetupGraph()
1795 for part in self.part_table.values():
1797 if not part.is_enabled():
1800 if part.is_foreign():
1803 graph = self.setup_graph(part)
1805 # Create nTDSConnection objects, routing replication traffic
1806 # around "failed" DCs.
1807 found_failed = False
1809 connected, found_failed = self.create_connections(graph,
1812 DEBUG("with detect_failed: connected %s Found failed %s" %
1813 (connected, found_failed))
1815 all_connected = False
1818 # One or more failed DCs preclude use of the ideal NC
1819 # replica graph. Add connections for the ideal graph.
1820 self.create_connections(graph, part, False)
1822 return all_connected
1824 def intersite(self, ping):
1825 """Generate the inter-site KCC replica graph and nTDSConnections
1827 As per MS-ADTS 6.2.2.3.
1829 If self.readonly is False, the connections are added to self.samdb.
1831 Produces self.kept_connections which is a set of NTDS
1832 Connections that should be kept during subsequent pruning
1835 After this has run, all sites should be connected in a minimum
1838 :param ping: An oracle function of remote site availability
1839 :return (True or False): (True) if the produced NC replica
1840 graph connects all sites that need to be connected
1845 mysite = self.my_site
1846 all_connected = True
1848 DEBUG_FN("intersite(): enter")
1850 # Determine who is the ISTG
1852 mysite.select_istg(self.samdb, mydsa, ro=True)
1854 mysite.select_istg(self.samdb, mydsa, ro=False)
1856 # Test whether local site has topology disabled
1857 if mysite.is_intersite_topology_disabled():
1858 DEBUG_FN("intersite(): exit disabled all_connected=%d" %
1860 return all_connected
1862 if not mydsa.is_istg():
1863 DEBUG_FN("intersite(): exit not istg all_connected=%d" %
1865 return all_connected
1867 self.merge_failed_links(ping)
1869 # For each NC with an NC replica that "should be present" on the
1870 # local DC or "is present" on any DC in the same site as the
1871 # local DC, the KCC constructs a site graph--a precursor to an NC
1872 # replica graph. The site connectivity for a site graph is defined
1873 # by objects of class interSiteTransport, siteLink, and
1874 # siteLinkBridge in the config NC.
1876 all_connected = self.create_intersite_connections()
1878 DEBUG_FN("intersite(): exit all_connected=%d" % all_connected)
1879 return all_connected
1881 # This function currently does no actions. The reason being that we cannot
1882 # perform modifies in this way on the RODC.
1883 def update_rodc_connection(self, ro=True):
1884 """Updates the RODC NTFRS connection object.
1886 If the local DSA is not an RODC, this does nothing.
1888 if not self.my_dsa.is_ro():
1891 # Given an nTDSConnection object cn1, such that cn1.options contains
1892 # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1893 # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1894 # that the following is true:
1896 # cn1.fromServer = cn2.fromServer
1897 # cn1.schedule = cn2.schedule
1899 # If no such cn2 can be found, cn1 is not modified.
1900 # If no such cn1 can be found, nothing is modified by this task.
1902 all_connections = self.my_dsa.connect_table.values()
1903 ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1904 rw_connections = [x for x in all_connections
1905 if x not in ro_connections]
1907 # XXX here we are dealing with multiple RODC_TOPO connections,
1908 # if they exist. It is not clear whether the spec means that
1909 # or if it ever arises.
1910 if rw_connections and ro_connections:
1911 for con in ro_connections:
1912 cn2 = rw_connections[0]
1913 con.from_dnstr = cn2.from_dnstr
1914 con.schedule = cn2.schedule
1915 con.to_be_modified = True
1917 self.my_dsa.commit_connections(self.samdb, ro=ro)
1919 def intrasite_max_node_edges(self, node_count):
1920 """Find the maximum number of edges directed to an intrasite node
1922 The KCC does not create more than 50 edges directed to a
1923 single DC. To optimize replication, we compute that each node
1924 should have n+2 total edges directed to it such that (n) is
1925 the smallest non-negative integer satisfying
1926 (node_count <= 2*(n*n) + 6*n + 7)
1928 (If the number of edges is m (i.e. n + 2), that is the same as
1929 2 * m*m - 2 * m + 3). We think in terms of n because that is
1930 the number of extra connections over the double directed ring
1931 that exists by default.
1941 :param node_count: total number of nodes in the replica graph
1943 The intention is that there should be no more than 3 hops
1944 between any two DSAs at a site. With up to 7 nodes the 2 edges
1945 of the ring are enough; any configuration of extra edges with
1946 8 nodes will be enough. It is less clear that the 3 hop
1947 guarantee holds at e.g. 15 nodes in degenerate cases, but
1948 those are quite unlikely given the extra edges are randomly
1951 :param node_count: the number of nodes in the site
1952 "return: The desired maximum number of connections
1956 if node_count <= (2 * (n * n) + (6 * n) + 7):
1964 def construct_intrasite_graph(self, site_local, dc_local,
1965 nc_x, gc_only, detect_stale):
1966 """Create an intrasite graph using given parameters
1968 This might be called a number of times per site with different
1971 Based on [MS-ADTS] 6.2.2.2
1973 :param site_local: site for which we are working
1974 :param dc_local: local DC that potentially needs a replica
1975 :param nc_x: naming context (x) that we are testing if it
1976 "should be present" on the local DC
1977 :param gc_only: Boolean - only consider global catalog servers
1978 :param detect_stale: Boolean - check whether links seems down
1981 # We're using the MS notation names here to allow
1982 # correlation back to the published algorithm.
1984 # nc_x - naming context (x) that we are testing if it
1985 # "should be present" on the local DC
1986 # f_of_x - replica (f) found on a DC (s) for NC (x)
1987 # dc_s - DC where f_of_x replica was found
1988 # dc_local - local DC that potentially needs a replica
1990 # r_list - replica list R
1991 # p_of_x - replica (p) is partial and found on a DC (s)
1993 # l_of_x - replica (l) is the local replica for NC (x)
1994 # that should appear on the local DC
1995 # r_len = is length of replica list |R|
1997 # If the DSA doesn't need a replica for this
1998 # partition (NC x) then continue
1999 needed, ro, partial = nc_x.should_be_present(dc_local)
2001 debug.DEBUG_YELLOW("construct_intrasite_graph(): enter" +
2002 "\n\tgc_only=%d" % gc_only +
2003 "\n\tdetect_stale=%d" % detect_stale +
2004 "\n\tneeded=%s" % needed +
2006 "\n\tpartial=%s" % partial +
2010 debug.DEBUG_RED("%s lacks 'should be present' status, "
2011 "aborting construct_intrasite_graph!" %
2015 # Create a NCReplica that matches what the local replica
2016 # should say. We'll use this below in our r_list
2017 l_of_x = NCReplica(dc_local, nc_x.nc_dnstr)
2019 l_of_x.identify_by_basedn(self.samdb)
2021 l_of_x.rep_partial = partial
2024 # Add this replica that "should be present" to the
2025 # needed replica table for this DSA
2026 dc_local.add_needed_replica(l_of_x)
2030 # Let R be a sequence containing each writable replica f of x
2031 # such that f "is present" on a DC s satisfying the following
2034 # * s is a writable DC other than the local DC.
2036 # * s is in the same site as the local DC.
2038 # * If x is a read-only full replica and x is a domain NC,
2039 # then the DC's functional level is at least
2040 # DS_BEHAVIOR_WIN2008.
2042 # * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
2043 # in the options attribute of the site settings object for
2044 # the local DC's site, or no tuple z exists in the
2045 # kCCFailedLinks or kCCFailedConnections variables such
2046 # that z.UUIDDsa is the objectGUID of the nTDSDSA object
2047 # for s, z.FailureCount > 0, and the current time -
2048 # z.TimeFirstFailure > 2 hours.
2052 # We'll loop thru all the DSAs looking for
2053 # writeable NC replicas that match the naming
2054 # context dn for (nc_x)
2056 for dc_s in self.my_site.dsa_table.values():
2057 # If this partition (nc_x) doesn't appear as a
2058 # replica (f_of_x) on (dc_s) then continue
2059 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2062 # Pull out the NCReplica (f) of (x) with the dn
2063 # that matches NC (x) we are examining.
2064 f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2066 # Replica (f) of NC (x) must be writable
2070 # Replica (f) of NC (x) must satisfy the
2071 # "is present" criteria for DC (s) that
2073 if not f_of_x.is_present():
2076 # DC (s) must be a writable DSA other than
2077 # my local DC. In other words we'd only replicate
2078 # from other writable DC
2079 if dc_s.is_ro() or dc_s is dc_local:
2082 # Certain replica graphs are produced only
2083 # for global catalogs, so test against
2084 # method input parameter
2085 if gc_only and not dc_s.is_gc():
2088 # DC (s) must be in the same site as the local DC
2089 # as this is the intra-site algorithm. This is
2090 # handled by virtue of placing DSAs in per
2091 # site objects (see enclosing for() loop)
2093 # If NC (x) is intended to be read-only full replica
2094 # for a domain NC on the target DC then the source
2095 # DC should have functional level at minimum WIN2008
2097 # Effectively we're saying that in order to replicate
2098 # to a targeted RODC (which was introduced in Windows 2008)
2099 # then we have to replicate from a DC that is also minimally
2102 # You can also see this requirement in the MS special
2103 # considerations for RODC which state that to deploy
2104 # an RODC, at least one writable domain controller in
2105 # the domain must be running Windows Server 2008
2106 if ro and not partial and nc_x.nc_type == NCType.domain:
2107 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2110 # If we haven't been told to turn off stale connection
2111 # detection and this dsa has a stale connection then
2113 if detect_stale and self.is_stale_link_connection(dc_s):
2116 # Replica meets criteria. Add it to table indexed
2117 # by the GUID of the DC that it appears on
2118 r_list.append(f_of_x)
2120 # If a partial (not full) replica of NC (x) "should be present"
2121 # on the local DC, append to R each partial replica (p of x)
2122 # such that p "is present" on a DC satisfying the same
2123 # criteria defined above for full replica DCs.
2125 # XXX This loop and the previous one differ only in whether
2126 # the replica is partial or not. here we only accept partial
2127 # (because we're partial); before we only accepted full. Order
2128 # doen't matter (the list is sorted a few lines down) so these
2129 # loops could easily be merged. Or this could be a helper
2133 # Now we loop thru all the DSAs looking for
2134 # partial NC replicas that match the naming
2135 # context dn for (NC x)
2136 for dc_s in self.my_site.dsa_table.values():
2138 # If this partition NC (x) doesn't appear as a
2139 # replica (p) of NC (x) on the dsa DC (s) then
2141 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2144 # Pull out the NCReplica with the dn that
2145 # matches NC (x) we are examining.
2146 p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2148 # Replica (p) of NC (x) must be partial
2149 if not p_of_x.is_partial():
2152 # Replica (p) of NC (x) must satisfy the
2153 # "is present" criteria for DC (s) that
2155 if not p_of_x.is_present():
2158 # DC (s) must be a writable DSA other than
2159 # my DSA. In other words we'd only replicate
2160 # from other writable DSA
2161 if dc_s.is_ro() or dc_s is dc_local:
2164 # Certain replica graphs are produced only
2165 # for global catalogs, so test against
2166 # method input parameter
2167 if gc_only and not dc_s.is_gc():
2170 # If we haven't been told to turn off stale connection
2171 # detection and this dsa has a stale connection then
2173 if detect_stale and self.is_stale_link_connection(dc_s):
2176 # Replica meets criteria. Add it to table indexed
2177 # by the GUID of the DSA that it appears on
2178 r_list.append(p_of_x)
2180 # Append to R the NC replica that "should be present"
2182 r_list.append(l_of_x)
2184 r_list.sort(key=lambda rep: ndr_pack(rep.rep_dsa_guid))
2187 max_node_edges = self.intrasite_max_node_edges(r_len)
2189 # Add a node for each r_list element to the replica graph
2192 node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2193 graph_list.append(node)
2195 # For each r(i) from (0 <= i < |R|-1)
2197 while i < (r_len - 1):
2198 # Add an edge from r(i) to r(i+1) if r(i) is a full
2199 # replica or r(i+1) is a partial replica
2200 if not r_list[i].is_partial() or r_list[i +1].is_partial():
2201 graph_list[i + 1].add_edge_from(r_list[i].rep_dsa_dnstr)
2203 # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2204 # replica or ri is a partial replica.
2205 if not r_list[i + 1].is_partial() or r_list[i].is_partial():
2206 graph_list[i].add_edge_from(r_list[i + 1].rep_dsa_dnstr)
2209 # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2210 # or r0 is a partial replica.
2211 if not r_list[r_len - 1].is_partial() or r_list[0].is_partial():
2212 graph_list[0].add_edge_from(r_list[r_len - 1].rep_dsa_dnstr)
2214 # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2215 # r|R|-1 is a partial replica.
2216 if not r_list[0].is_partial() or r_list[r_len -1].is_partial():
2217 graph_list[r_len - 1].add_edge_from(r_list[0].rep_dsa_dnstr)
2219 DEBUG("r_list is length %s" % len(r_list))
2220 DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr))
2223 do_dot_files = self.dot_file_dir is not None and self.debug
2224 if self.verify or do_dot_files:
2226 dot_vertices = set()
2227 for v1 in graph_list:
2228 dot_vertices.add(v1.dsa_dnstr)
2229 for v2 in v1.edge_from:
2230 dot_edges.append((v2, v1.dsa_dnstr))
2231 dot_vertices.add(v2)
2233 verify_properties = ('connected',)
2234 verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2235 label='%s__%s__%s' % (site_local.site_dnstr,
2236 nctype_lut[nc_x.nc_type],
2238 properties=verify_properties, debug=DEBUG,
2240 dot_file_dir=self.dot_file_dir,
2243 rw_dot_vertices = set(x for x in dot_vertices
2244 if not self.get_dsa(x).is_ro())
2245 rw_dot_edges = [(a, b) for a, b in dot_edges if
2246 a in rw_dot_vertices and b in rw_dot_vertices]
2247 rw_verify_properties = ('connected',
2248 'directed_double_ring_or_small')
2249 verify_and_dot('intrasite_rw_pre_ntdscon', rw_dot_edges,
2251 label='%s__%s__%s' % (site_local.site_dnstr,
2252 nctype_lut[nc_x.nc_type],
2254 properties=rw_verify_properties, debug=DEBUG,
2256 dot_file_dir=self.dot_file_dir,
2259 # For each existing nTDSConnection object implying an edge
2260 # from rj of R to ri such that j != i, an edge from rj to ri
2261 # is not already in the graph, and the total edges directed
2262 # to ri is less than n+2, the KCC adds that edge to the graph.
2263 for vertex in graph_list:
2264 dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2265 for connect in dsa.connect_table.values():
2266 remote = connect.from_dnstr
2267 if remote in self.my_site.dsa_table:
2268 vertex.add_edge_from(remote)
2270 DEBUG('reps are: %s' % ' '.join(x.rep_dsa_dnstr for x in r_list))
2271 DEBUG('dsas are: %s' % ' '.join(x.dsa_dnstr for x in graph_list))
2273 for tnode in graph_list:
2274 # To optimize replication latency in sites with many NC
2275 # replicas, the KCC adds new edges directed to ri to bring
2276 # the total edges to n+2, where the NC replica rk of R
2277 # from which the edge is directed is chosen at random such
2278 # that k != i and an edge from rk to ri is not already in
2281 # Note that the KCC tech ref does not give a number for
2282 # the definition of "sites with many NC replicas". At a
2283 # bare minimum to satisfy n+2 edges directed at a node we
2284 # have to have at least three replicas in |R| (i.e. if n
2285 # is zero then at least replicas from two other graph
2286 # nodes may direct edges to us).
2287 if r_len >= 3 and not tnode.has_sufficient_edges():
2288 candidates = [x for x in graph_list if
2290 x.dsa_dnstr not in tnode.edge_from)]
2292 debug.DEBUG_BLUE("looking for random link for %s. r_len %d, "
2293 "graph len %d candidates %d"
2294 % (tnode.dsa_dnstr, r_len, len(graph_list),
2297 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2299 while candidates and not tnode.has_sufficient_edges():
2300 other = random.choice(candidates)
2301 DEBUG("trying to add candidate %s" % other.dsa_dnstr)
2302 if not tnode.add_edge_from(other.dsa_dnstr):
2303 debug.DEBUG_RED("could not add %s" % other.dsa_dnstr)
2304 candidates.remove(other)
2306 DEBUG_FN("not adding links to %s: nodes %s, links is %s/%s" %
2307 (tnode.dsa_dnstr, r_len, len(tnode.edge_from),
2310 # Print the graph node in debug mode
2311 DEBUG_FN("%s" % tnode)
2313 # For each edge directed to the local DC, ensure a nTDSConnection
2314 # points to us that satisfies the KCC criteria
2316 if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2317 tnode.add_connections_from_edges(dc_local, self.ip_transport)
2319 if self.verify or do_dot_files:
2321 dot_vertices = set()
2322 for v1 in graph_list:
2323 dot_vertices.add(v1.dsa_dnstr)
2324 for v2 in v1.edge_from:
2325 dot_edges.append((v2, v1.dsa_dnstr))
2326 dot_vertices.add(v2)
2328 verify_properties = ('connected',)
2329 verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2330 label='%s__%s__%s' % (site_local.site_dnstr,
2331 nctype_lut[nc_x.nc_type],
2333 properties=verify_properties, debug=DEBUG,
2335 dot_file_dir=self.dot_file_dir,
2338 rw_dot_vertices = set(x for x in dot_vertices
2339 if not self.get_dsa(x).is_ro())
2340 rw_dot_edges = [(a, b) for a, b in dot_edges if
2341 a in rw_dot_vertices and b in rw_dot_vertices]
2342 rw_verify_properties = ('connected',
2343 'directed_double_ring_or_small')
2344 verify_and_dot('intrasite_rw_post_ntdscon', rw_dot_edges,
2346 label='%s__%s__%s' % (site_local.site_dnstr,
2347 nctype_lut[nc_x.nc_type],
2349 properties=rw_verify_properties, debug=DEBUG,
2351 dot_file_dir=self.dot_file_dir,
2354 def intrasite(self):
2355 """Generate the intrasite KCC connections
2357 As per MS-ADTS 6.2.2.2.
2359 If self.readonly is False, the connections are added to self.samdb.
2361 After this call, all DCs in each site with more than 3 DCs
2362 should be connected in a bidirectional ring. If a site has 2
2363 DCs, they will bidirectionally connected. Sites with many DCs
2364 may have arbitrary extra connections.
2370 DEBUG_FN("intrasite(): enter")
2372 # Test whether local site has topology disabled
2373 mysite = self.my_site
2374 if mysite.is_intrasite_topology_disabled():
2377 detect_stale = (not mysite.is_detect_stale_disabled())
2378 for connect in mydsa.connect_table.values():
2379 if connect.to_be_added:
2380 debug.DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2382 # Loop thru all the partitions, with gc_only False
2383 for partdn, part in self.part_table.items():
2384 self.construct_intrasite_graph(mysite, mydsa, part, False,
2386 for connect in mydsa.connect_table.values():
2387 if connect.to_be_added:
2388 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2390 # If the DC is a GC server, the KCC constructs an additional NC
2391 # replica graph (and creates nTDSConnection objects) for the
2392 # config NC as above, except that only NC replicas that "are present"
2393 # on GC servers are added to R.
2394 for connect in mydsa.connect_table.values():
2395 if connect.to_be_added:
2396 debug.DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2398 # Do it again, with gc_only True
2399 for partdn, part in self.part_table.items():
2400 if part.is_config():
2401 self.construct_intrasite_graph(mysite, mydsa, part, True,
2404 # The DC repeats the NC replica graph computation and nTDSConnection
2405 # creation for each of the NC replica graphs, this time assuming
2406 # that no DC has failed. It does so by re-executing the steps as
2407 # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2408 # set in the options attribute of the site settings object for
2409 # the local DC's site. (ie. we set "detec_stale" flag to False)
2410 for connect in mydsa.connect_table.values():
2411 if connect.to_be_added:
2412 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2414 # Loop thru all the partitions.
2415 for partdn, part in self.part_table.items():
2416 self.construct_intrasite_graph(mysite, mydsa, part, False,
2417 False) # don't detect stale
2419 # If the DC is a GC server, the KCC constructs an additional NC
2420 # replica graph (and creates nTDSConnection objects) for the
2421 # config NC as above, except that only NC replicas that "are present"
2422 # on GC servers are added to R.
2423 for connect in mydsa.connect_table.values():
2424 if connect.to_be_added:
2425 debug.DEBUG_RED("TO BE ADDED:\n%s" % connect)
2427 for partdn, part in self.part_table.items():
2428 if part.is_config():
2429 self.construct_intrasite_graph(mysite, mydsa, part, True,
2430 False) # don't detect stale
2432 self._commit_changes(mydsa)
2434 def list_dsas(self):
2435 """Compile a comprehensive list of DSA DNs
2437 These are all the DSAs on all the sites that KCC would be
2440 This method is not idempotent and may not work correctly in
2441 sequence with KCC.run().
2443 :return: a list of DSA DN strings.
2448 self.load_all_sites()
2449 self.load_all_partitions()
2450 self.load_ip_transport()
2451 self.load_all_sitelinks()
2453 for site in self.site_table.values():
2454 dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2455 for dsa in site.dsa_table.values()])
2458 def load_samdb(self, dburl, lp, creds, force=False):
2459 """Load the database using an url, loadparm, and credentials
2461 If force is False, the samdb won't be reloaded if it already
2464 :param dburl: a database url.
2465 :param lp: a loadparm object.
2466 :param creds: a Credentials object.
2467 :param force: a boolean indicating whether to overwrite.
2470 if force or self.samdb is None:
2472 self.samdb = SamDB(url=dburl,
2473 session_info=system_session(),
2474 credentials=creds, lp=lp)
2475 except ldb.LdbError as e1:
2476 (num, msg) = e1.args
2477 raise KCCError("Unable to open sam database %s : %s" %
2480 def plot_all_connections(self, basename, verify_properties=()):
2481 """Helper function to plot and verify NTDSConnections
2483 :param basename: an identifying string to use in filenames and logs.
2484 :param verify_properties: properties to verify (default empty)
2486 verify = verify_properties and self.verify
2487 if not verify and self.dot_file_dir is None:
2495 for dsa in self.dsa_by_dnstr.values():
2496 dot_vertices.append(dsa.dsa_dnstr)
2498 vertex_colours.append('#cc0000')
2500 vertex_colours.append('#0000cc')
2501 for con in dsa.connect_table.values():
2502 if con.is_rodc_topology():
2503 edge_colours.append('red')
2505 edge_colours.append('blue')
2506 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2508 verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2509 label=self.my_dsa_dnstr,
2510 properties=verify_properties, debug=DEBUG,
2511 verify=verify, dot_file_dir=self.dot_file_dir,
2512 directed=True, edge_colors=edge_colours,
2513 vertex_colors=vertex_colours)
2515 def run(self, dburl, lp, creds, forced_local_dsa=None,
2516 forget_local_links=False, forget_intersite_links=False,
2517 attempt_live_connections=False):
2518 """Perform a KCC run, possibly updating repsFrom topology
2520 :param dburl: url of the database to work with.
2521 :param lp: a loadparm object.
2522 :param creds: a Credentials object.
2523 :param forced_local_dsa: pretend to be on the DSA with this dn_str
2524 :param forget_local_links: calculate as if no connections existed
2525 (boolean, default False)
2526 :param forget_intersite_links: calculate with only intrasite connection
2527 (boolean, default False)
2528 :param attempt_live_connections: attempt to connect to remote DSAs to
2529 determine link availability (boolean, default False)
2530 :return: 1 on error, 0 otherwise
2532 if self.samdb is None:
2533 DEBUG_FN("samdb is None; let's load it from %s" % (dburl,))
2534 self.load_samdb(dburl, lp, creds, force=False)
2536 if forced_local_dsa:
2537 self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" %
2545 self.load_all_sites()
2546 self.load_all_partitions()
2547 self.load_ip_transport()
2548 self.load_all_sitelinks()
2550 if self.verify or self.dot_file_dir is not None:
2552 for site in self.site_table.values():
2553 guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2555 in site.dsa_table.items())
2557 self.plot_all_connections('dsa_initial')
2560 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2561 for dnstr, c_rep in current_reps.items():
2562 DEBUG("c_rep %s" % c_rep)
2563 dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2565 verify_and_dot('dsa_repsFrom_initial', dot_edges,
2566 directed=True, label=self.my_dsa_dnstr,
2567 properties=(), debug=DEBUG, verify=self.verify,
2568 dot_file_dir=self.dot_file_dir)
2571 for site in self.site_table.values():
2572 for dsa in site.dsa_table.values():
2573 current_reps, needed_reps = dsa.get_rep_tables()
2574 for dn_str, rep in current_reps.items():
2575 for reps_from in rep.rep_repsFrom:
2576 DEBUG("rep %s" % rep)
2577 dsa_guid = str(reps_from.source_dsa_obj_guid)
2578 dsa_dn = guid_to_dnstr[dsa_guid]
2579 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2581 verify_and_dot('dsa_repsFrom_initial_all', dot_edges,
2582 directed=True, label=self.my_dsa_dnstr,
2583 properties=(), debug=DEBUG, verify=self.verify,
2584 dot_file_dir=self.dot_file_dir)
2588 for link in self.sitelink_table.values():
2589 from hashlib import md5
2590 tmp_str = link.dnstr.encode('utf8')
2591 colour = '#' + md5(tmp_str).hexdigest()[:6]
2592 for a, b in itertools.combinations(link.site_list, 2):
2593 dot_edges.append((a[1], b[1]))
2594 dot_colours.append(colour)
2595 properties = ('connected',)
2596 verify_and_dot('dsa_sitelink_initial', dot_edges,
2598 label=self.my_dsa_dnstr, properties=properties,
2599 debug=DEBUG, verify=self.verify,
2600 dot_file_dir=self.dot_file_dir,
2601 edge_colors=dot_colours)
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 as 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 as e: