1 # define the KCC object
3 # Copyright (C) Dave Craft 2011
4 # Copyright (C) Andrew Bartlett 2015
6 # Andrew Bartlett's alleged work performed by his underlings Douglas
7 # Bagnall and Garming Sam.
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 from samba import unix2nttime, nttime2unix
27 from samba import ldb, dsdb, drs_utils
28 from samba.auth import system_session
29 from samba.samdb import SamDB
30 from samba.dcerpc import drsuapi, misc
32 from samba.kcc.kcc_utils import Site, Partition, Transport, SiteLink
33 from samba.kcc.kcc_utils import NCReplica, NCType, nctype_lut, GraphNode
34 from samba.kcc.kcc_utils import RepsFromTo, KCCError, KCCFailedObject
35 from samba.kcc.graph import convert_schedule_to_repltimes
37 from samba.ndr import ndr_pack
39 from samba.kcc.graph_utils import verify_and_dot
41 from samba.kcc import ldif_import_export
42 from samba.kcc.graph import setup_graph, get_spanning_tree_edges
43 from samba.kcc.graph import Vertex
45 from samba.kcc.debug import DEBUG, DEBUG_FN, logger
46 from samba.kcc import debug
49 def sort_replica_by_dsa_guid(rep1, rep2):
50 """Helper to sort NCReplicas by their DSA guids
52 The guids need to be sorted in their NDR form.
54 :param rep1: An NC replica
55 :param rep2: Another replica
56 :return: -1, 0, or 1, indicating sort order.
58 return cmp(ndr_pack(rep1.rep_dsa_guid), ndr_pack(rep2.rep_dsa_guid))
61 def sort_dsa_by_gc_and_guid(dsa1, dsa2):
62 """Helper to sort DSAs by guid global catalog status
64 GC DSAs come before non-GC DSAs, other than that, the guids are
67 :param dsa1: A DSA object
68 :param dsa2: Another DSA
69 :return: -1, 0, or 1, indicating sort order.
71 if dsa1.is_gc() and not dsa2.is_gc():
73 if not dsa1.is_gc() and dsa2.is_gc():
75 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
78 def is_smtp_replication_available():
79 """Can the KCC use SMTP replication?
81 Currently always returns false because Samba doesn't implement
82 SMTP transfer for NC changes between DCs.
84 :return: Boolean (always False)
90 """The Knowledge Consistency Checker class.
92 A container for objects and methods allowing a run of the KCC. Produces a
93 set of connections in the samdb for which the Distributed Replication
94 Service can then utilize to replicate naming contexts
96 :param unix_now: The putative current time in seconds since 1970.
97 :param read_only: Don't write to the database.
98 :param verify: Check topological invariants for the generated graphs
99 :param debug: Write verbosely to stderr.
100 "param dot_file_dir: write diagnostic Graphviz files in this directory
102 def __init__(self, unix_now, readonly=False, verify=False, debug=False,
104 """Initializes the partitions class which can hold
105 our local DCs partitions or all the partitions in
108 self.part_table = {} # partition objects
110 self.ip_transport = None
111 self.sitelink_table = {}
112 self.dsa_by_dnstr = {}
113 self.dsa_by_guid = {}
115 self.get_dsa_by_guidstr = self.dsa_by_guid.get
116 self.get_dsa = self.dsa_by_dnstr.get
118 # TODO: These should be backed by a 'permanent' store so that when
119 # calling DRSGetReplInfo with DS_REPL_INFO_KCC_DSA_CONNECT_FAILURES,
120 # the failure information can be returned
121 self.kcc_failed_links = {}
122 self.kcc_failed_connections = set()
124 # Used in inter-site topology computation. A list
125 # of connections (by NTDSConnection object) that are
126 # to be kept when pruning un-needed NTDS Connections
127 self.kept_connections = set()
129 self.my_dsa_dnstr = None # My dsa DN
130 self.my_dsa = None # My dsa object
132 self.my_site_dnstr = None
137 self.unix_now = unix_now
138 self.nt_now = unix2nttime(unix_now)
139 self.readonly = readonly
142 self.dot_file_dir = dot_file_dir
144 def load_ip_transport(self):
145 """Loads the inter-site transport objects for Sites
148 :raise KCCError: if no IP transport is found
151 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
152 self.samdb.get_config_basedn(),
153 scope=ldb.SCOPE_SUBTREE,
154 expression="(objectClass=interSiteTransport)")
155 except ldb.LdbError, (enum, estr):
156 raise KCCError("Unable to find inter-site transports - (%s)" %
162 transport = Transport(dnstr)
164 transport.load_transport(self.samdb)
165 if transport.name == 'IP':
166 self.ip_transport = transport
167 elif transport.name == 'SMTP':
168 logger.info("Samba KCC is ignoring the obsolete SMTP transport.")
171 logger.warning("Samba KCC does not support the transport called %r."
174 if self.ip_transport is None:
175 raise KCCError("there doesn't seem to be an IP transport")
177 def load_all_sitelinks(self):
178 """Loads the inter-site siteLink objects
181 :raise KCCError: if site-links aren't found
184 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
185 self.samdb.get_config_basedn(),
186 scope=ldb.SCOPE_SUBTREE,
187 expression="(objectClass=siteLink)")
188 except ldb.LdbError, (enum, estr):
189 raise KCCError("Unable to find inter-site siteLinks - (%s)" % estr)
195 if dnstr in self.sitelink_table:
198 sitelink = SiteLink(dnstr)
200 sitelink.load_sitelink(self.samdb)
202 # Assign this siteLink to table
204 self.sitelink_table[dnstr] = sitelink
206 def load_site(self, dn_str):
207 """Helper for load_my_site and load_all_sites.
209 Put all the site's DSAs into the KCC indices.
211 :param dn_str: a site dn_str
212 :return: the Site object pertaining to the dn_str
214 site = Site(dn_str, self.unix_now)
215 site.load_site(self.samdb)
217 # We avoid replacing the site with an identical copy in case
218 # somewhere else has a reference to the old one, which would
219 # lead to all manner of confusion and chaos.
220 guid = str(site.site_guid)
221 if guid not in self.site_table:
222 self.site_table[guid] = site
223 self.dsa_by_dnstr.update(site.dsa_table)
224 self.dsa_by_guid.update((str(x.dsa_guid), x)
225 for x in site.dsa_table.values())
227 return self.site_table[guid]
229 def load_my_site(self):
230 """Load the Site object for the local DSA.
234 self.my_site_dnstr = ("CN=%s,CN=Sites,%s" % (
235 self.samdb.server_site_name(),
236 self.samdb.get_config_basedn()))
238 self.my_site = self.load_site(self.my_site_dnstr)
240 def load_all_sites(self):
241 """Discover all sites and create Site objects.
244 :raise: KCCError if sites can't be found
247 res = self.samdb.search("CN=Sites,%s" %
248 self.samdb.get_config_basedn(),
249 scope=ldb.SCOPE_SUBTREE,
250 expression="(objectClass=site)")
251 except ldb.LdbError, (enum, estr):
252 raise KCCError("Unable to find sites - (%s)" % estr)
255 sitestr = str(msg.dn)
256 self.load_site(sitestr)
258 def load_my_dsa(self):
259 """Discover my nTDSDSA dn thru the rootDSE entry
262 :raise: KCCError if DSA can't be found
264 dn = ldb.Dn(self.samdb, "<GUID=%s>" % self.samdb.get_ntds_GUID())
266 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
267 attrs=["objectGUID"])
268 except ldb.LdbError, (enum, estr):
269 logger.warning("Search for %s failed: %s. This typically happens"
270 " in --importldif mode due to lack of module"
271 " support.", dn, estr)
273 # We work around the failure above by looking at the
274 # dsServiceName that was put in the fake rootdse by
275 # the --exportldif, rather than the
276 # samdb.get_ntds_GUID(). The disadvantage is that this
277 # mode requires we modify the @ROOTDSE dnq to support
279 service_name_res = self.samdb.search(base="",
280 scope=ldb.SCOPE_BASE,
281 attrs=["dsServiceName"])
282 dn = ldb.Dn(self.samdb,
283 service_name_res[0]["dsServiceName"][0])
285 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
286 attrs=["objectGUID"])
287 except ldb.LdbError, (enum, estr):
288 raise KCCError("Unable to find my nTDSDSA - (%s)" % estr)
291 raise KCCError("Unable to find my nTDSDSA at %s" %
294 ntds_guid = misc.GUID(self.samdb.get_ntds_GUID())
295 if misc.GUID(res[0]["objectGUID"][0]) != ntds_guid:
296 raise KCCError("Did not find the GUID we expected,"
297 " perhaps due to --importldif")
299 self.my_dsa_dnstr = str(res[0].dn)
301 self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
303 if self.my_dsa_dnstr not in self.dsa_by_dnstr:
304 debug.DEBUG_DARK_YELLOW("my_dsa %s isn't in self.dsas_by_dnstr:"
305 " it must be RODC.\n"
306 "Let's add it, because my_dsa is special!"
307 "\n(likewise for self.dsa_by_guid)" %
310 self.dsa_by_dnstr[self.my_dsa_dnstr] = self.my_dsa
311 self.dsa_by_guid[str(self.my_dsa.dsa_guid)] = self.my_dsa
313 def load_all_partitions(self):
314 """Discover and load all partitions.
316 Each NC is inserted into the part_table by partition
317 dn string (not the nCName dn string)
320 :raise: KCCError if partitions can't be found
323 res = self.samdb.search("CN=Partitions,%s" %
324 self.samdb.get_config_basedn(),
325 scope=ldb.SCOPE_SUBTREE,
326 expression="(objectClass=crossRef)")
327 except ldb.LdbError, (enum, estr):
328 raise KCCError("Unable to find partitions - (%s)" % estr)
331 partstr = str(msg.dn)
334 if partstr in self.part_table:
337 part = Partition(partstr)
339 part.load_partition(self.samdb)
340 self.part_table[partstr] = part
342 def refresh_failed_links_connections(self, ping=None):
343 """Ensure the failed links list is up to date
345 Based on MS-ADTS 6.2.2.1
347 :param ping: An oracle function of remote site availability
350 # LINKS: Refresh failed links
351 self.kcc_failed_links = {}
352 current, needed = self.my_dsa.get_rep_tables()
353 for replica in current.values():
354 # For every possible connection to replicate
355 for reps_from in replica.rep_repsFrom:
356 failure_count = reps_from.consecutive_sync_failures
357 if failure_count <= 0:
360 dsa_guid = str(reps_from.source_dsa_obj_guid)
361 time_first_failure = reps_from.last_success
362 last_result = reps_from.last_attempt
363 dns_name = reps_from.dns_name1
365 f = self.kcc_failed_links.get(dsa_guid)
367 f = KCCFailedObject(dsa_guid, failure_count,
368 time_first_failure, last_result,
370 self.kcc_failed_links[dsa_guid] = f
372 f.failure_count = max(f.failure_count, failure_count)
373 f.time_first_failure = min(f.time_first_failure,
375 f.last_result = last_result
377 # CONNECTIONS: Refresh failed connections
378 restore_connections = set()
380 DEBUG("refresh_failed_links: checking if links are still down")
381 for connection in self.kcc_failed_connections:
382 if ping(connection.dns_name):
383 # Failed connection is no longer failing
384 restore_connections.add(connection)
386 connection.failure_count += 1
388 DEBUG("refresh_failed_links: not checking live links because we\n"
389 "weren't asked to --attempt-live-connections")
391 # Remove the restored connections from the failed connections
392 self.kcc_failed_connections.difference_update(restore_connections)
394 def is_stale_link_connection(self, target_dsa):
395 """Check whether a link to a remote DSA is stale
397 Used in MS-ADTS 6.2.2.2 Intrasite Connection Creation
399 Returns True if the remote seems to have been down for at
400 least two hours, otherwise False.
402 :param target_dsa: the remote DSA object
403 :return: True if link is stale, otherwise False
405 failed_link = self.kcc_failed_links.get(str(target_dsa.dsa_guid))
407 # failure_count should be > 0, but check anyways
408 if failed_link.failure_count > 0:
409 unix_first_failure = \
410 nttime2unix(failed_link.time_first_failure)
411 # TODO guard against future
412 if unix_first_failure > self.unix_now:
413 logger.error("The last success time attribute for \
414 repsFrom is in the future!")
416 # Perform calculation in seconds
417 if (self.unix_now - unix_first_failure) > 60 * 60 * 2:
421 # We have checked failed *links*, but we also need to check
426 # TODO: This should be backed by some form of local database
427 def remove_unneeded_failed_links_connections(self):
428 # Remove all tuples in kcc_failed_links where failure count = 0
429 # In this implementation, this should never happen.
431 # Remove all connections which were not used this run or connections
432 # that became active during this run.
435 def _ensure_connections_are_loaded(self, connections):
436 """Load or fake-load NTDSConnections lacking GUIDs
438 New connections don't have GUIDs and created times which are
439 needed for sorting. If we're in read-only mode, we make fake
440 GUIDs, otherwise we ask SamDB to do it for us.
442 :param connections: an iterable of NTDSConnection objects.
445 for cn_conn in connections:
446 if cn_conn.guid is None:
448 cn_conn.guid = misc.GUID(str(uuid.uuid4()))
449 cn_conn.whenCreated = self.nt_now
451 cn_conn.load_connection(self.samdb)
453 def _mark_broken_ntdsconn(self):
454 """Find NTDS Connections that lack a remote
456 I'm not sure how they appear. Let's be rid of them by marking
457 them with the to_be_deleted attribute.
461 for cn_conn in self.my_dsa.connect_table.values():
462 s_dnstr = cn_conn.get_from_dnstr()
464 DEBUG_FN("%s has phantom connection %s" % (self.my_dsa,
466 cn_conn.to_be_deleted = True
468 def _mark_unneeded_local_ntdsconn(self):
469 """Find unneeded intrasite NTDS Connections for removal
471 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections.
472 Every DC removes its own unnecessary intrasite connections.
473 This function tags them with the to_be_deleted attribute.
477 # XXX should an RODC be regarded as same site? It isn't part
478 # of the intrasite ring.
480 if self.my_site.is_cleanup_ntdsconn_disabled():
481 DEBUG_FN("not doing ntdsconn cleanup for site %s, "
482 "because it is disabled" % self.my_site)
487 self._ensure_connections_are_loaded(mydsa.connect_table.values())
489 local_connections = []
491 for cn_conn in mydsa.connect_table.values():
492 s_dnstr = cn_conn.get_from_dnstr()
493 if s_dnstr in self.my_site.dsa_table:
494 removable = not (cn_conn.is_generated() or
495 cn_conn.is_rodc_topology())
496 packed_guid = ndr_pack(cn_conn.guid)
497 local_connections.append((cn_conn, s_dnstr,
498 packed_guid, removable))
500 for a, b in itertools.permutations(local_connections, 2):
501 cn_conn, s_dnstr, packed_guid, removable = a
502 cn_conn2, s_dnstr2, packed_guid2, removable2 = b
504 s_dnstr == s_dnstr2 and
505 cn_conn.whenCreated < cn_conn2.whenCreated or
506 (cn_conn.whenCreated == cn_conn2.whenCreated and
507 packed_guid < packed_guid2)):
508 cn_conn.to_be_deleted = True
510 def _mark_unneeded_intersite_ntdsconn(self):
511 """find unneeded intersite NTDS Connections for removal
513 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections. The
514 intersite topology generator removes links for all DCs in its
515 site. Here we just tag them with the to_be_deleted attribute.
519 # Find the intersite connections
520 local_dsas = self.my_site.dsa_table
521 connections_and_dsas = []
522 for dsa in local_dsas.values():
523 for cn in dsa.connect_table.values():
524 s_dnstr = cn.get_from_dnstr()
525 if s_dnstr not in local_dsas:
526 from_dsa = self.get_dsa(s_dnstr)
527 connections_and_dsas.append((cn, dsa, from_dsa))
529 self._ensure_connections_are_loaded(x[0] for x in connections_and_dsas)
530 for cn, to_dsa, from_dsa in connections_and_dsas:
531 if not cn.is_generated() or cn.is_rodc_topology():
534 # If the connection is in the kept_connections list, we
535 # only remove it if an endpoint seems down.
536 if (cn in self.kept_connections and
537 not (self.is_bridgehead_failed(to_dsa, True) or
538 self.is_bridgehead_failed(from_dsa, True))):
541 # this one is broken and might be superseded by another.
542 # But which other? Let's just say another link to the same
543 # site can supersede.
544 from_dnstr = from_dsa.dsa_dnstr
545 for site in self.site_table.values():
546 if from_dnstr in site.rw_dsa_table:
547 for cn2, to_dsa2, from_dsa2 in connections_and_dsas:
548 if (cn is not cn2 and
549 from_dsa2 in site.rw_dsa_table):
550 cn.to_be_deleted = True
552 def _commit_changes(self, dsa):
553 if dsa.is_ro() or self.readonly:
554 for connect in dsa.connect_table.values():
555 if connect.to_be_deleted:
556 logger.info("TO BE DELETED:\n%s" % connect)
557 if connect.to_be_added:
558 logger.info("TO BE ADDED:\n%s" % connect)
559 if connect.to_be_modified:
560 logger.info("TO BE MODIFIED:\n%s" % connect)
562 # Peform deletion from our tables but perform
563 # no database modification
564 dsa.commit_connections(self.samdb, ro=True)
566 # Commit any modified connections
567 dsa.commit_connections(self.samdb)
569 def remove_unneeded_ntdsconn(self, all_connected):
570 """Remove unneeded NTDS Connections once topology is calculated
572 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections
574 :param all_connected: indicates whether all sites are connected
577 self._mark_broken_ntdsconn()
578 self._mark_unneeded_local_ntdsconn()
579 # if we are not the istg, we're done!
580 # if we are the istg, but all_connected is False, we also do nothing.
581 if self.my_dsa.is_istg() and all_connected:
582 self._mark_unneeded_intersite_ntdsconn()
584 for dsa in self.my_site.dsa_table.values():
585 self._commit_changes(dsa)
588 def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
589 """Update an repsFrom object if required.
591 Part of MS-ADTS 6.2.2.5.
593 Update t_repsFrom if necessary to satisfy requirements. Such
594 updates are typically required when the IDL_DRSGetNCChanges
595 server has moved from one site to another--for example, to
596 enable compression when the server is moved from the
597 client's site to another site.
599 The repsFrom.update_flags bit field may be modified
600 auto-magically if any changes are made here. See
601 kcc_utils.RepsFromTo for gory details.
604 :param n_rep: NC replica we need
605 :param t_repsFrom: repsFrom tuple to modify
606 :param s_rep: NC replica at source DSA
607 :param s_dsa: source DSA
608 :param cn_conn: Local DSA NTDSConnection child
612 s_dnstr = s_dsa.dsa_dnstr
613 same_site = s_dnstr in self.my_site.dsa_table
615 # if schedule doesn't match then update and modify
616 times = convert_schedule_to_repltimes(cn_conn.schedule)
617 if times != t_repsFrom.schedule:
618 t_repsFrom.schedule = times
620 # Bit DRS_PER_SYNC is set in replicaFlags if and only
621 # if nTDSConnection schedule has a value v that specifies
622 # scheduled replication is to be performed at least once
624 if cn_conn.is_schedule_minimum_once_per_week():
626 if ((t_repsFrom.replica_flags &
627 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0):
628 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
630 # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
631 # if the source DSA and the local DC's nTDSDSA object are
632 # in the same site or source dsa is the FSMO role owner
633 # of one or more FSMO roles in the NC replica.
634 if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
636 if ((t_repsFrom.replica_flags &
637 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0):
638 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
640 # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
641 # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
642 # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
643 # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
644 # t.replicaFlags if and only if s and the local DC's
645 # nTDSDSA object are in different sites.
646 if ((cn_conn.options &
647 dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0):
649 if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
652 # it LOOKS as if this next test is a bit silly: it
653 # checks the flag then sets it if it not set; the same
654 # effect could be achieved by unconditionally setting
655 # it. But in fact the repsFrom object has special
656 # magic attached to it, and altering replica_flags has
657 # side-effects. That is bad in my opinion, but there
659 if ((t_repsFrom.replica_flags &
660 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
661 t_repsFrom.replica_flags |= \
662 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
666 if ((t_repsFrom.replica_flags &
667 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
668 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
670 # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
671 # and only if s and the local DC's nTDSDSA object are
672 # not in the same site and the
673 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
674 # clear in cn!options
675 if (not same_site and
677 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
679 if ((t_repsFrom.replica_flags &
680 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0):
681 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
683 # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
684 # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
685 if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
687 if ((t_repsFrom.replica_flags &
688 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0):
689 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
691 # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
692 # set in t.replicaFlags if and only if cn!enabledConnection = false.
693 if not cn_conn.is_enabled():
695 if ((t_repsFrom.replica_flags &
696 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0):
697 t_repsFrom.replica_flags |= \
698 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
700 if ((t_repsFrom.replica_flags &
701 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0):
702 t_repsFrom.replica_flags |= \
703 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
705 # If s and the local DC's nTDSDSA object are in the same site,
706 # cn!transportType has no value, or the RDN of cn!transportType
709 # Bit DRS_MAIL_REP in t.replicaFlags is clear.
711 # t.uuidTransport = NULL GUID.
713 # t.uuidDsa = The GUID-based DNS name of s.
717 # Bit DRS_MAIL_REP in t.replicaFlags is set.
719 # If x is the object with dsname cn!transportType,
720 # t.uuidTransport = x!objectGUID.
722 # Let a be the attribute identified by
723 # x!transportAddressAttribute. If a is
724 # the dNSHostName attribute, t.uuidDsa = the GUID-based
725 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
727 # It appears that the first statement i.e.
729 # "If s and the local DC's nTDSDSA object are in the same
730 # site, cn!transportType has no value, or the RDN of
731 # cn!transportType is CN=IP:"
733 # could be a slightly tighter statement if it had an "or"
734 # between each condition. I believe this should
737 # IF (same-site) OR (no-value) OR (type-ip)
739 # because IP should be the primary transport mechanism
740 # (even in inter-site) and the absense of the transportType
741 # attribute should always imply IP no matter if its multi-site
743 # NOTE MS-TECH INCORRECT:
745 # All indications point to these statements above being
746 # incorrectly stated:
748 # t.uuidDsa = The GUID-based DNS name of s.
750 # Let a be the attribute identified by
751 # x!transportAddressAttribute. If a is
752 # the dNSHostName attribute, t.uuidDsa = the GUID-based
753 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
755 # because the uuidDSA is a GUID and not a GUID-base DNS
756 # name. Nor can uuidDsa hold (s!parent)!a if not
757 # dNSHostName. What should have been said is:
759 # t.naDsa = The GUID-based DNS name of s
761 # That would also be correct if transportAddressAttribute
762 # were "mailAddress" because (naDsa) can also correctly
763 # hold the SMTP ISM service address.
765 nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
767 if ((t_repsFrom.replica_flags &
768 drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0):
769 t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
771 t_repsFrom.transport_guid = misc.GUID()
773 # See (NOTE MS-TECH INCORRECT) above
775 # NOTE: it looks like these conditionals are pointless,
776 # because the state will end up as `t_repsFrom.dns_name1 ==
777 # nastr` in either case, BUT the repsFrom thing is magic and
778 # assigning to it alters some flags. So we try not to update
779 # it unless necessary.
780 if t_repsFrom.dns_name1 != nastr:
781 t_repsFrom.dns_name1 = nastr
783 if t_repsFrom.version > 0x1 and t_repsFrom.dns_name2 != nastr:
784 t_repsFrom.dns_name2 = nastr
786 if t_repsFrom.is_modified():
787 DEBUG_FN("modify_repsFrom(): %s" % t_repsFrom)
789 def get_dsa_for_implied_replica(self, n_rep, cn_conn):
790 """If a connection imply a replica, find the relevant DSA
792 Given a NC replica and NTDS Connection, determine if the
793 connection implies a repsFrom tuple should be present from the
794 source DSA listed in the connection to the naming context. If
795 it should be, return the DSA; otherwise return None.
797 Based on part of MS-ADTS 6.2.2.5
799 :param n_rep: NC replica
800 :param cn_conn: NTDS Connection
801 :return: source DSA or None
803 # XXX different conditions for "implies" than MS-ADTS 6.2.2
806 # It boils down to: we want an enabled, non-FRS connections to
807 # a valid remote DSA with a non-RO replica corresponding to
810 if not cn_conn.is_enabled() or cn_conn.is_rodc_topology():
813 s_dnstr = cn_conn.get_from_dnstr()
814 s_dsa = self.get_dsa(s_dnstr)
816 # No DSA matching this source DN string?
820 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
822 if (s_rep is not None and
823 s_rep.is_present() and
824 (not s_rep.is_ro() or n_rep.is_partial())):
829 def translate_ntdsconn(self, current_dsa=None):
830 """Adjust repsFrom to match NTDSConnections
832 This function adjusts values of repsFrom abstract attributes of NC
833 replicas on the local DC to match those implied by
834 nTDSConnection objects.
836 Based on [MS-ADTS] 6.2.2.5
838 :param current_dsa: optional DSA on whose behalf we are acting.
843 if current_dsa is None:
844 current_dsa = self.my_dsa
846 if current_dsa.is_translate_ntdsconn_disabled():
847 DEBUG_FN("skipping translate_ntdsconn() "
848 "because disabling flag is set")
851 DEBUG_FN("translate_ntdsconn(): enter")
853 current_rep_table, needed_rep_table = current_dsa.get_rep_tables()
855 # Filled in with replicas we currently have that need deleting
858 # We're using the MS notation names here to allow
859 # correlation back to the published algorithm.
861 # n_rep - NC replica (n)
862 # t_repsFrom - tuple (t) in n!repsFrom
863 # s_dsa - Source DSA of the replica. Defined as nTDSDSA
864 # object (s) such that (s!objectGUID = t.uuidDsa)
865 # In our IDL representation of repsFrom the (uuidDsa)
866 # attribute is called (source_dsa_obj_guid)
867 # cn_conn - (cn) is nTDSConnection object and child of the local
868 # DC's nTDSDSA object and (cn!fromServer = s)
869 # s_rep - source DSA replica of n
871 # If we have the replica and its not needed
872 # then we add it to the "to be deleted" list.
873 for dnstr in current_rep_table:
874 if dnstr not in needed_rep_table:
875 delete_reps.add(dnstr)
877 DEBUG_FN('current %d needed %d delete %d' % (len(current_rep_table),
878 len(needed_rep_table), len(delete_reps)))
881 DEBUG('deleting these reps: %s' % delete_reps)
882 for dnstr in delete_reps:
883 del current_rep_table[dnstr]
885 # Now perform the scan of replicas we'll need
886 # and compare any current repsFrom against the
888 for n_rep in needed_rep_table.values():
890 # load any repsFrom and fsmo roles as we'll
891 # need them during connection translation
892 n_rep.load_repsFrom(self.samdb)
893 n_rep.load_fsmo_roles(self.samdb)
895 # Loop thru the existing repsFrom tupples (if any)
896 # XXX This is a list and could contain duplicates
897 # (multiple load_repsFrom calls)
898 for t_repsFrom in n_rep.rep_repsFrom:
900 # for each tuple t in n!repsFrom, let s be the nTDSDSA
901 # object such that s!objectGUID = t.uuidDsa
902 guidstr = str(t_repsFrom.source_dsa_obj_guid)
903 s_dsa = self.get_dsa_by_guidstr(guidstr)
905 # Source dsa is gone from config (strange)
906 # so cleanup stale repsFrom for unlisted DSA
908 logger.warning("repsFrom source DSA guid (%s) not found" %
910 t_repsFrom.to_be_deleted = True
913 # Find the connection that this repsFrom would use. If
914 # there isn't a good one (i.e. non-RODC_TOPOLOGY,
915 # meaning non-FRS), we delete the repsFrom.
916 s_dnstr = s_dsa.dsa_dnstr
917 connections = current_dsa.get_connection_by_from_dnstr(s_dnstr)
918 for cn_conn in connections:
919 if not cn_conn.is_rodc_topology():
922 # no break means no non-rodc_topology connection exists
923 t_repsFrom.to_be_deleted = True
926 # KCC removes this repsFrom tuple if any of the following
928 # No NC replica of the NC "is present" on DSA that
929 # would be source of replica
931 # A writable replica of the NC "should be present" on
932 # the local DC, but a partial replica "is present" on
934 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
936 if s_rep is None or not s_rep.is_present() or \
937 (not n_rep.is_ro() and s_rep.is_partial()):
939 t_repsFrom.to_be_deleted = True
942 # If the KCC did not remove t from n!repsFrom, it updates t
943 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
945 # Loop thru connections and add implied repsFrom tuples
946 # for each NTDSConnection under our local DSA if the
947 # repsFrom is not already present
948 for cn_conn in current_dsa.connect_table.values():
950 s_dsa = self.get_dsa_for_implied_replica(n_rep, cn_conn)
954 # Loop thru the existing repsFrom tupples (if any) and
955 # if we already have a tuple for this connection then
956 # no need to proceed to add. It will have been changed
957 # to have the correct attributes above
958 for t_repsFrom in n_rep.rep_repsFrom:
959 guidstr = str(t_repsFrom.source_dsa_obj_guid)
960 if s_dsa is self.get_dsa_by_guidstr(guidstr):
967 # Create a new RepsFromTo and proceed to modify
968 # it according to specification
969 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
971 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
973 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
975 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
977 # Add to our NC repsFrom as this is newly computed
978 if t_repsFrom.is_modified():
979 n_rep.rep_repsFrom.append(t_repsFrom)
982 # Display any to be deleted or modified repsFrom
983 text = n_rep.dumpstr_to_be_deleted()
985 logger.info("TO BE DELETED:\n%s" % text)
986 text = n_rep.dumpstr_to_be_modified()
988 logger.info("TO BE MODIFIED:\n%s" % text)
990 # Peform deletion from our tables but perform
991 # no database modification
992 n_rep.commit_repsFrom(self.samdb, ro=True)
994 # Commit any modified repsFrom to the NC replica
995 n_rep.commit_repsFrom(self.samdb)
997 def merge_failed_links(self, ping=None):
998 """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
1000 The KCC on a writable DC attempts to merge the link and connection
1001 failure information from bridgehead DCs in its own site to help it
1002 identify failed bridgehead DCs.
1004 Based on MS-ADTS 6.2.2.3.2 "Merge of kCCFailedLinks and kCCFailedLinks
1007 :param ping: An oracle of current bridgehead availability
1010 # 1. Queries every bridgehead server in your site (other than yourself)
1011 # 2. For every ntDSConnection that references a server in a different
1012 # site merge all the failure info
1014 # XXX - not implemented yet
1015 if ping is not None:
1016 debug.DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1018 DEBUG_FN("skipping merge_failed_links() because it requires "
1019 "real network connections\n"
1020 "and we weren't asked to --attempt-live-connections")
1022 def setup_graph(self, part):
1023 """Set up an intersite graph
1025 An intersite graph has a Vertex for each site object, a
1026 MultiEdge for each SiteLink object, and a MutliEdgeSet for
1027 each siteLinkBridge object (or implied siteLinkBridge). It
1028 reflects the intersite topology in a slightly more abstract
1031 Roughly corresponds to MS-ADTS 6.2.2.3.4.3
1033 :param part: a Partition object
1034 :returns: an InterSiteGraph object
1036 # If 'Bridge all site links' is enabled and Win2k3 bridges required
1038 # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1039 # No documentation for this however, ntdsapi.h appears to have:
1040 # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1041 bridges_required = self.my_site.site_options & 0x00001002 != 0
1042 transport_guid = str(self.ip_transport.guid)
1044 g = setup_graph(part, self.site_table, transport_guid,
1045 self.sitelink_table, bridges_required)
1047 if self.verify or self.dot_file_dir is not None:
1049 for edge in g.edges:
1050 for a, b in itertools.combinations(edge.vertices, 2):
1051 dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1052 verify_properties = ()
1053 name = 'site_edges_%s' % part.partstr
1054 verify_and_dot(name, dot_edges, directed=False,
1055 label=self.my_dsa_dnstr,
1056 properties=verify_properties, debug=DEBUG,
1058 dot_file_dir=self.dot_file_dir)
1062 def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1063 """Get a bridghead DC for a site.
1065 Part of MS-ADTS 6.2.2.3.4.4
1067 :param site: site object representing for which a bridgehead
1069 :param part: crossRef for NC to replicate.
1070 :param transport: interSiteTransport object for replication
1072 :param partial_ok: True if a DC containing a partial
1073 replica or a full replica will suffice, False if only
1074 a full replica will suffice.
1075 :param detect_failed: True to detect failed DCs and route
1076 replication traffic around them, False to assume no DC
1078 :return: dsa object for the bridgehead DC or None
1081 bhs = self.get_all_bridgeheads(site, part, transport,
1082 partial_ok, detect_failed)
1084 debug.DEBUG_MAGENTA("get_bridgehead FAILED:\nsitedn = %s" %
1088 debug.DEBUG_GREEN("get_bridgehead:\n\tsitedn = %s\n\tbhdn = %s" %
1089 (site.site_dnstr, bhs[0].dsa_dnstr))
1092 def get_all_bridgeheads(self, site, part, transport,
1093 partial_ok, detect_failed):
1094 """Get all bridghead DCs on a site satisfying the given criteria
1096 Part of MS-ADTS 6.2.2.3.4.4
1098 :param site: site object representing the site for which
1099 bridgehead DCs are desired.
1100 :param part: partition for NC to replicate.
1101 :param transport: interSiteTransport object for
1102 replication traffic.
1103 :param partial_ok: True if a DC containing a partial
1104 replica or a full replica will suffice, False if
1105 only a full replica will suffice.
1106 :param detect_failed: True to detect failed DCs and route
1107 replication traffic around them, FALSE to assume
1109 :return: list of dsa object for available bridgehead DCs
1113 if transport.name != "IP":
1114 raise KCCError("get_all_bridgeheads has run into a "
1115 "non-IP transport! %r"
1116 % (transport.name,))
1118 DEBUG_FN(site.rw_dsa_table)
1119 for dsa in site.rw_dsa_table.values():
1121 pdnstr = dsa.get_parent_dnstr()
1123 # IF t!bridgeheadServerListBL has one or more values and
1124 # t!bridgeheadServerListBL does not contain a reference
1125 # to the parent object of dc then skip dc
1126 if ((len(transport.bridgehead_list) != 0 and
1127 pdnstr not in transport.bridgehead_list)):
1130 # IF dc is in the same site as the local DC
1131 # IF a replica of cr!nCName is not in the set of NC replicas
1132 # that "should be present" on dc or a partial replica of the
1133 # NC "should be present" but partialReplicasOkay = FALSE
1135 if self.my_site.same_site(dsa):
1136 needed, ro, partial = part.should_be_present(dsa)
1137 if not needed or (partial and not partial_ok):
1139 rep = dsa.get_current_replica(part.nc_dnstr)
1142 # IF an NC replica of cr!nCName is not in the set of NC
1143 # replicas that "are present" on dc or a partial replica of
1144 # the NC "is present" but partialReplicasOkay = FALSE
1147 rep = dsa.get_current_replica(part.nc_dnstr)
1148 if rep is None or (rep.is_partial() and not partial_ok):
1151 # IF AmIRODC() and cr!nCName corresponds to default NC then
1152 # Let dsaobj be the nTDSDSA object of the dc
1153 # IF dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1155 if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1156 if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1159 # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1161 if self.is_bridgehead_failed(dsa, detect_failed):
1162 DEBUG("bridgehead is failed")
1165 DEBUG_FN("found a bridgehead: %s" % dsa.dsa_dnstr)
1168 # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1170 # SORT bhs such that all GC servers precede DCs that are not GC
1171 # servers, and otherwise by ascending objectGUID
1173 # SORT bhs in a random order
1174 if site.is_random_bridgehead_disabled():
1175 bhs.sort(sort_dsa_by_gc_and_guid)
1178 debug.DEBUG_YELLOW(bhs)
1181 def is_bridgehead_failed(self, dsa, detect_failed):
1182 """Determine whether a given DC is known to be in a failed state
1184 :param dsa: the bridgehead to test
1185 :param detect_failed: True to really check, False to assume no failure
1186 :return: True if and only if the DC should be considered failed
1188 Here we DEPART from the pseudo code spec which appears to be
1189 wrong. It says, in full:
1191 /***** BridgeheadDCFailed *****/
1192 /* Determine whether a given DC is known to be in a failed state.
1193 * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1194 * IN: detectFailedDCs - TRUE if and only failed DC detection is
1196 * RETURNS: TRUE if and only if the DC should be considered to be in a
1199 BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1201 IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1202 the options attribute of the site settings object for the local
1205 ELSEIF a tuple z exists in the kCCFailedLinks or
1206 kCCFailedConnections variables such that z.UUIDDsa =
1207 objectGUID, z.FailureCount > 1, and the current time -
1208 z.TimeFirstFailure > 2 hours
1211 RETURN detectFailedDCs
1215 where you will see detectFailedDCs is not behaving as
1216 advertised -- it is acting as a default return code in the
1217 event that a failure is not detected, not a switch turning
1218 detection on or off. Elsewhere the documentation seems to
1219 concur with the comment rather than the code.
1221 if not detect_failed:
1224 # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1225 # When DETECT_STALE_DISABLED, we can never know of if
1226 # it's in a failed state
1227 if self.my_site.site_options & 0x00000008:
1230 return self.is_stale_link_connection(dsa)
1232 def create_connection(self, part, rbh, rsite, transport,
1233 lbh, lsite, link_opt, link_sched,
1234 partial_ok, detect_failed):
1235 """Create an nTDSConnection object as specified if it doesn't exist.
1237 Part of MS-ADTS 6.2.2.3.4.5
1239 :param part: crossRef object for the NC to replicate.
1240 :param rbh: nTDSDSA object for DC to act as the
1241 IDL_DRSGetNCChanges server (which is in a site other
1242 than the local DC's site).
1243 :param rsite: site of the rbh
1244 :param transport: interSiteTransport object for the transport
1245 to use for replication traffic.
1246 :param lbh: nTDSDSA object for DC to act as the
1247 IDL_DRSGetNCChanges client (which is in the local DC's site).
1248 :param lsite: site of the lbh
1249 :param link_opt: Replication parameters (aggregated siteLink options,
1251 :param link_sched: Schedule specifying the times at which
1252 to begin replicating.
1253 :partial_ok: True if bridgehead DCs containing partial
1254 replicas of the NC are acceptable.
1255 :param detect_failed: True to detect failed DCs and route
1256 replication traffic around them, FALSE to assume no DC
1259 rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1261 rbh_table = dict((x.dsa_dnstr, x) for x in rbhs_all)
1263 debug.DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all),
1264 [x.dsa_dnstr for x in rbhs_all]))
1266 # MS-TECH says to compute rbhs_avail but then doesn't use it
1267 # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1268 # partial_ok, detect_failed)
1270 lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1273 lbhs_all.append(lbh)
1275 debug.DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all),
1276 [x.dsa_dnstr for x in lbhs_all]))
1278 # MS-TECH says to compute lbhs_avail but then doesn't use it
1279 # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1280 # partial_ok, detect_failed)
1282 # FOR each nTDSConnection object cn such that the parent of cn is
1283 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1284 for ldsa in lbhs_all:
1285 for cn in ldsa.connect_table.values():
1287 rdsa = rbh_table.get(cn.from_dnstr)
1291 debug.DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1292 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1293 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1294 # cn!transportType references t
1295 if ((cn.is_generated() and
1296 not cn.is_rodc_topology() and
1297 cn.transport_guid == transport.guid)):
1299 # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1300 # cn!options and cn!schedule != sch
1301 # Perform an originating update to set cn!schedule to
1303 if ((not cn.is_user_owned_schedule() and
1304 not cn.is_equivalent_schedule(link_sched))):
1305 cn.schedule = link_sched
1306 cn.set_modified(True)
1308 # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1309 # NTDSCONN_OPT_USE_NOTIFY are set in cn
1310 if cn.is_override_notify_default() and \
1313 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1315 # Perform an originating update to clear bits
1316 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1317 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1318 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1320 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1321 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1322 cn.set_modified(True)
1327 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1329 # Perform an originating update to set bits
1330 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1331 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1332 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1334 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1335 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1336 cn.set_modified(True)
1338 # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1339 if cn.is_twoway_sync():
1341 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1343 # Perform an originating update to clear bit
1344 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1345 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1346 cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1347 cn.set_modified(True)
1352 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1354 # Perform an originating update to set bit
1355 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1356 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1357 cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1358 cn.set_modified(True)
1360 # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1362 if cn.is_intersite_compression_disabled():
1364 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1366 # Perform an originating update to clear bit
1367 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1370 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0):
1372 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1373 cn.set_modified(True)
1377 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1379 # Perform an originating update to set bit
1380 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1383 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1385 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1386 cn.set_modified(True)
1388 # Display any modified connection
1390 if cn.to_be_modified:
1391 logger.info("TO BE MODIFIED:\n%s" % cn)
1393 ldsa.commit_connections(self.samdb, ro=True)
1395 ldsa.commit_connections(self.samdb)
1398 valid_connections = 0
1400 # FOR each nTDSConnection object cn such that cn!parent is
1401 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1402 for ldsa in lbhs_all:
1403 for cn in ldsa.connect_table.values():
1405 rdsa = rbh_table.get(cn.from_dnstr)
1409 debug.DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1411 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1412 # cn!transportType references t) and
1413 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1414 if (((not cn.is_generated() or
1415 cn.transport_guid == transport.guid) and
1416 not cn.is_rodc_topology())):
1418 # LET rguid be the objectGUID of the nTDSDSA object
1419 # referenced by cn!fromServer
1420 # LET lguid be (cn!parent)!objectGUID
1422 # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1423 # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1424 # Increment cValidConnections by 1
1425 if ((not self.is_bridgehead_failed(rdsa, detect_failed) and
1426 not self.is_bridgehead_failed(ldsa, detect_failed))):
1427 valid_connections += 1
1429 # IF keepConnections does not contain cn!objectGUID
1430 # APPEND cn!objectGUID to keepConnections
1431 self.kept_connections.add(cn)
1434 debug.DEBUG_RED("valid connections %d" % valid_connections)
1435 DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1436 # IF cValidConnections = 0
1437 if valid_connections == 0:
1439 # LET opt be NTDSCONN_OPT_IS_GENERATED
1440 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1442 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1443 # SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1444 # NTDSCONN_OPT_USE_NOTIFY in opt
1445 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1446 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1447 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1449 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1450 # SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1451 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1452 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1454 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1456 # SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1458 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1459 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1461 # Perform an originating update to create a new nTDSConnection
1462 # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1463 # cn!options = opt, cn!transportType is a reference to t,
1464 # cn!fromServer is a reference to rbh, and cn!schedule = sch
1465 DEBUG_FN("new connection, KCC dsa: %s" % self.my_dsa.dsa_dnstr)
1466 system_flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1467 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1469 cn = lbh.new_connection(opt, system_flags, transport,
1470 rbh.dsa_dnstr, link_sched)
1472 # Display any added connection
1475 logger.info("TO BE ADDED:\n%s" % cn)
1477 lbh.commit_connections(self.samdb, ro=True)
1479 lbh.commit_connections(self.samdb)
1481 # APPEND cn!objectGUID to keepConnections
1482 self.kept_connections.add(cn)
1484 def add_transports(self, vertex, local_vertex, graph, detect_failed):
1485 """Build a Vertex's transport lists
1487 Each vertex has accept_red_red and accept_black lists that
1488 list what transports they accept under various conditions. The
1489 only transport that is ever accepted is IP, and a dummy extra
1490 transport called "EDGE_TYPE_ALL".
1492 Part of MS-ADTS 6.2.2.3.4.3 -- ColorVertices
1494 :param vertex: the remote vertex we are thinking about
1495 :param local_vertex: the vertex relating to the local site.
1496 :param graph: the intersite graph
1497 :param detect_failed: whether to detect failed links
1498 :return: True if some bridgeheads were not found
1500 # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1501 # here, but using vertex seems to make more sense. That is,
1502 # the docs want this:
1504 #bh = self.get_bridgehead(local_vertex.site, vertex.part, transport,
1505 # local_vertex.is_black(), detect_failed)
1509 vertex.accept_red_red = []
1510 vertex.accept_black = []
1511 found_failed = False
1513 if vertex in graph.connected_vertices:
1514 t_guid = str(self.ip_transport.guid)
1516 bh = self.get_bridgehead(vertex.site, vertex.part,
1518 vertex.is_black(), detect_failed)
1520 if vertex.site.is_rodc_site():
1521 vertex.accept_red_red.append(t_guid)
1525 vertex.accept_red_red.append(t_guid)
1526 vertex.accept_black.append(t_guid)
1528 # Add additional transport to ensure another run of Dijkstra
1529 vertex.accept_red_red.append("EDGE_TYPE_ALL")
1530 vertex.accept_black.append("EDGE_TYPE_ALL")
1534 def create_connections(self, graph, part, detect_failed):
1535 """Create intersite NTDSConnections as needed by a partition
1537 Construct an NC replica graph for the NC identified by
1538 the given crossRef, then create any additional nTDSConnection
1541 :param graph: site graph.
1542 :param part: crossRef object for NC.
1543 :param detect_failed: True to detect failed DCs and route
1544 replication traffic around them, False to assume no DC
1547 Modifies self.kept_connections by adding any connections
1548 deemed to be "in use".
1550 :return: (all_connected, found_failed_dc)
1551 (all_connected) True if the resulting NC replica graph
1552 connects all sites that need to be connected.
1553 (found_failed_dc) True if one or more failed DCs were
1556 all_connected = True
1557 found_failed = False
1559 DEBUG_FN("create_connections(): enter\n"
1560 "\tpartdn=%s\n\tdetect_failed=%s" %
1561 (part.nc_dnstr, detect_failed))
1563 # XXX - This is a highly abbreviated function from the MS-TECH
1564 # ref. It creates connections between bridgeheads to all
1565 # sites that have appropriate replicas. Thus we are not
1566 # creating a minimum cost spanning tree but instead
1567 # producing a fully connected tree. This should produce
1568 # a full (albeit not optimal cost) replication topology.
1570 my_vertex = Vertex(self.my_site, part)
1571 my_vertex.color_vertex()
1573 for v in graph.vertices:
1575 if self.add_transports(v, my_vertex, graph, detect_failed):
1578 # No NC replicas for this NC in the site of the local DC,
1579 # so no nTDSConnection objects need be created
1580 if my_vertex.is_white():
1581 return all_connected, found_failed
1583 edge_list, n_components = get_spanning_tree_edges(graph,
1587 DEBUG_FN("%s Number of components: %d" %
1588 (part.nc_dnstr, n_components))
1589 if n_components > 1:
1590 all_connected = False
1592 # LET partialReplicaOkay be TRUE if and only if
1593 # localSiteVertex.Color = COLOR.BLACK
1594 partial_ok = my_vertex.is_black()
1596 # Utilize the IP transport only for now
1597 transport = self.ip_transport
1599 DEBUG("edge_list %s" % edge_list)
1601 # XXX more accurate comparison?
1602 if e.directed and e.vertices[0].site is self.my_site:
1605 if e.vertices[0].site is self.my_site:
1606 rsite = e.vertices[1].site
1608 rsite = e.vertices[0].site
1610 # We don't make connections to our own site as that
1611 # is intrasite topology generator's job
1612 if rsite is self.my_site:
1613 DEBUG("rsite is my_site")
1616 # Determine bridgehead server in remote site
1617 rbh = self.get_bridgehead(rsite, part, transport,
1618 partial_ok, detect_failed)
1622 # RODC acts as an BH for itself
1624 # LET lbh be the nTDSDSA object of the local DC
1626 # LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1627 # cr, t, partialReplicaOkay, detectFailedDCs)
1628 if self.my_dsa.is_ro():
1629 lsite = self.my_site
1632 lsite = self.my_site
1633 lbh = self.get_bridgehead(lsite, part, transport,
1634 partial_ok, detect_failed)
1637 debug.DEBUG_RED("DISASTER! lbh is None")
1640 DEBUG_FN("lsite: %s\nrsite: %s" % (lsite, rsite))
1641 DEBUG_FN("vertices %s" % (e.vertices,))
1642 debug.DEBUG_BLUE("bridgeheads\n%s\n%s\n%s" % (lbh, rbh, "-" * 70))
1644 sitelink = e.site_link
1645 if sitelink is None:
1649 link_opt = sitelink.options
1650 link_sched = sitelink.schedule
1652 self.create_connection(part, rbh, rsite, transport,
1653 lbh, lsite, link_opt, link_sched,
1654 partial_ok, detect_failed)
1656 return all_connected, found_failed
1658 def create_intersite_connections(self):
1659 """Create NTDSConnections as necessary for all partitions.
1661 Computes an NC replica graph for each NC replica that "should be
1662 present" on the local DC or "is present" on any DC in the same site
1663 as the local DC. For each edge directed to an NC replica on such a
1664 DC from an NC replica on a DC in another site, the KCC creates an
1665 nTDSConnection object to imply that edge if one does not already
1668 Modifies self.kept_connections - A set of nTDSConnection
1669 objects for edges that are directed
1670 to the local DC's site in one or more NC replica graphs.
1672 :return: True if spanning trees were created for all NC replica
1673 graphs, otherwise False.
1675 all_connected = True
1676 self.kept_connections = set()
1678 # LET crossRefList be the set containing each object o of class
1679 # crossRef such that o is a child of the CN=Partitions child of the
1682 # FOR each crossRef object cr in crossRefList
1683 # IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1684 # is clear in cr!systemFlags, skip cr.
1685 # LET g be the GRAPH return of SetupGraph()
1687 for part in self.part_table.values():
1689 if not part.is_enabled():
1692 if part.is_foreign():
1695 graph = self.setup_graph(part)
1697 # Create nTDSConnection objects, routing replication traffic
1698 # around "failed" DCs.
1699 found_failed = False
1701 connected, found_failed = self.create_connections(graph,
1704 DEBUG("with detect_failed: connected %s Found failed %s" %
1705 (connected, found_failed))
1707 all_connected = False
1710 # One or more failed DCs preclude use of the ideal NC
1711 # replica graph. Add connections for the ideal graph.
1712 self.create_connections(graph, part, False)
1714 return all_connected
1716 def intersite(self, ping):
1717 """Generate the inter-site KCC replica graph and nTDSConnections
1719 As per MS-ADTS 6.2.2.3.
1721 If self.readonly is False, the connections are added to self.samdb.
1723 Produces self.kept_connections which is a set of NTDS
1724 Connections that should be kept during subsequent pruning
1727 After this has run, all sites should be connected in a minimum
1730 :param ping: An oracle function of remote site availability
1731 :return (True or False): (True) if the produced NC replica
1732 graph connects all sites that need to be connected
1737 mysite = self.my_site
1738 all_connected = True
1740 DEBUG_FN("intersite(): enter")
1742 # Determine who is the ISTG
1744 mysite.select_istg(self.samdb, mydsa, ro=True)
1746 mysite.select_istg(self.samdb, mydsa, ro=False)
1748 # Test whether local site has topology disabled
1749 if mysite.is_intersite_topology_disabled():
1750 DEBUG_FN("intersite(): exit disabled all_connected=%d" %
1752 return all_connected
1754 if not mydsa.is_istg():
1755 DEBUG_FN("intersite(): exit not istg all_connected=%d" %
1757 return all_connected
1759 self.merge_failed_links(ping)
1761 # For each NC with an NC replica that "should be present" on the
1762 # local DC or "is present" on any DC in the same site as the
1763 # local DC, the KCC constructs a site graph--a precursor to an NC
1764 # replica graph. The site connectivity for a site graph is defined
1765 # by objects of class interSiteTransport, siteLink, and
1766 # siteLinkBridge in the config NC.
1768 all_connected = self.create_intersite_connections()
1770 DEBUG_FN("intersite(): exit all_connected=%d" % all_connected)
1771 return all_connected
1773 def update_rodc_connection(self):
1774 """Updates the RODC NTFRS connection object.
1776 If the local DSA is not an RODC, this does nothing.
1778 if not self.my_dsa.is_ro():
1781 # Given an nTDSConnection object cn1, such that cn1.options contains
1782 # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1783 # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1784 # that the following is true:
1786 # cn1.fromServer = cn2.fromServer
1787 # cn1.schedule = cn2.schedule
1789 # If no such cn2 can be found, cn1 is not modified.
1790 # If no such cn1 can be found, nothing is modified by this task.
1792 all_connections = self.my_dsa.connect_table.values()
1793 ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1794 rw_connections = [x for x in all_connections
1795 if x not in ro_connections]
1797 # XXX here we are dealing with multiple RODC_TOPO connections,
1798 # if they exist. It is not clear whether the spec means that
1799 # or if it ever arises.
1800 if rw_connections and ro_connections:
1801 for con in ro_connections:
1802 cn2 = rw_connections[0]
1803 con.from_dnstr = cn2.from_dnstr
1804 con.schedule = cn2.schedule
1805 con.to_be_modified = True
1807 self.my_dsa.commit_connections(self.samdb, ro=self.readonly)
1809 def intrasite_max_node_edges(self, node_count):
1810 """Find the maximum number of edges directed to an intrasite node
1812 The KCC does not create more than 50 edges directed to a
1813 single DC. To optimize replication, we compute that each node
1814 should have n+2 total edges directed to it such that (n) is
1815 the smallest non-negative integer satisfying
1816 (node_count <= 2*(n*n) + 6*n + 7)
1818 (If the number of edges is m (i.e. n + 2), that is the same as
1819 2 * m*m - 2 * m + 3). We think in terms of n because that is
1820 the number of extra connections over the double directed ring
1821 that exists by default.
1831 :param node_count: total number of nodes in the replica graph
1833 The intention is that there should be no more than 3 hops
1834 between any two DSAs at a site. With up to 7 nodes the 2 edges
1835 of the ring are enough; any configuration of extra edges with
1836 8 nodes will be enough. It is less clear that the 3 hop
1837 guarantee holds at e.g. 15 nodes in degenerate cases, but
1838 those are quite unlikely given the extra edges are randomly
1841 :param node_count: the number of nodes in the site
1842 "return: The desired maximum number of connections
1846 if node_count <= (2 * (n * n) + (6 * n) + 7):
1854 def construct_intrasite_graph(self, site_local, dc_local,
1855 nc_x, gc_only, detect_stale):
1856 """Create an intrasite graph using given parameters
1858 This might be called a number of times per site with different
1861 Based on [MS-ADTS] 6.2.2.2
1863 :param site_local: site for which we are working
1864 :param dc_local: local DC that potentially needs a replica
1865 :param nc_x: naming context (x) that we are testing if it
1866 "should be present" on the local DC
1867 :param gc_only: Boolean - only consider global catalog servers
1868 :param detect_stale: Boolean - check whether links seems down
1871 # We're using the MS notation names here to allow
1872 # correlation back to the published algorithm.
1874 # nc_x - naming context (x) that we are testing if it
1875 # "should be present" on the local DC
1876 # f_of_x - replica (f) found on a DC (s) for NC (x)
1877 # dc_s - DC where f_of_x replica was found
1878 # dc_local - local DC that potentially needs a replica
1880 # r_list - replica list R
1881 # p_of_x - replica (p) is partial and found on a DC (s)
1883 # l_of_x - replica (l) is the local replica for NC (x)
1884 # that should appear on the local DC
1885 # r_len = is length of replica list |R|
1887 # If the DSA doesn't need a replica for this
1888 # partition (NC x) then continue
1889 needed, ro, partial = nc_x.should_be_present(dc_local)
1891 debug.DEBUG_YELLOW("construct_intrasite_graph(): enter" +
1892 "\n\tgc_only=%d" % gc_only +
1893 "\n\tdetect_stale=%d" % detect_stale +
1894 "\n\tneeded=%s" % needed +
1896 "\n\tpartial=%s" % partial +
1900 debug.DEBUG_RED("%s lacks 'should be present' status, "
1901 "aborting construct_intersite_graph!" %
1905 # Create a NCReplica that matches what the local replica
1906 # should say. We'll use this below in our r_list
1907 l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid,
1910 l_of_x.identify_by_basedn(self.samdb)
1912 l_of_x.rep_partial = partial
1915 # Add this replica that "should be present" to the
1916 # needed replica table for this DSA
1917 dc_local.add_needed_replica(l_of_x)
1921 # Let R be a sequence containing each writable replica f of x
1922 # such that f "is present" on a DC s satisfying the following
1925 # * s is a writable DC other than the local DC.
1927 # * s is in the same site as the local DC.
1929 # * If x is a read-only full replica and x is a domain NC,
1930 # then the DC's functional level is at least
1931 # DS_BEHAVIOR_WIN2008.
1933 # * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
1934 # in the options attribute of the site settings object for
1935 # the local DC's site, or no tuple z exists in the
1936 # kCCFailedLinks or kCCFailedConnections variables such
1937 # that z.UUIDDsa is the objectGUID of the nTDSDSA object
1938 # for s, z.FailureCount > 0, and the current time -
1939 # z.TimeFirstFailure > 2 hours.
1943 # We'll loop thru all the DSAs looking for
1944 # writeable NC replicas that match the naming
1945 # context dn for (nc_x)
1947 for dc_s in self.my_site.dsa_table.values():
1948 # If this partition (nc_x) doesn't appear as a
1949 # replica (f_of_x) on (dc_s) then continue
1950 if not nc_x.nc_dnstr in dc_s.current_rep_table:
1953 # Pull out the NCReplica (f) of (x) with the dn
1954 # that matches NC (x) we are examining.
1955 f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
1957 # Replica (f) of NC (x) must be writable
1961 # Replica (f) of NC (x) must satisfy the
1962 # "is present" criteria for DC (s) that
1964 if not f_of_x.is_present():
1967 # DC (s) must be a writable DSA other than
1968 # my local DC. In other words we'd only replicate
1969 # from other writable DC
1970 if dc_s.is_ro() or dc_s is dc_local:
1973 # Certain replica graphs are produced only
1974 # for global catalogs, so test against
1975 # method input parameter
1976 if gc_only and not dc_s.is_gc():
1979 # DC (s) must be in the same site as the local DC
1980 # as this is the intra-site algorithm. This is
1981 # handled by virtue of placing DSAs in per
1982 # site objects (see enclosing for() loop)
1984 # If NC (x) is intended to be read-only full replica
1985 # for a domain NC on the target DC then the source
1986 # DC should have functional level at minimum WIN2008
1988 # Effectively we're saying that in order to replicate
1989 # to a targeted RODC (which was introduced in Windows 2008)
1990 # then we have to replicate from a DC that is also minimally
1993 # You can also see this requirement in the MS special
1994 # considerations for RODC which state that to deploy
1995 # an RODC, at least one writable domain controller in
1996 # the domain must be running Windows Server 2008
1997 if ro and not partial and nc_x.nc_type == NCType.domain:
1998 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2001 # If we haven't been told to turn off stale connection
2002 # detection and this dsa has a stale connection then
2004 if detect_stale and self.is_stale_link_connection(dc_s):
2007 # Replica meets criteria. Add it to table indexed
2008 # by the GUID of the DC that it appears on
2009 r_list.append(f_of_x)
2011 # If a partial (not full) replica of NC (x) "should be present"
2012 # on the local DC, append to R each partial replica (p of x)
2013 # such that p "is present" on a DC satisfying the same
2014 # criteria defined above for full replica DCs.
2016 # XXX This loop and the previous one differ only in whether
2017 # the replica is partial or not. here we only accept partial
2018 # (because we're partial); before we only accepted full. Order
2019 # doen't matter (the list is sorted a few lines down) so these
2020 # loops could easily be merged. Or this could be a helper
2024 # Now we loop thru all the DSAs looking for
2025 # partial NC replicas that match the naming
2026 # context dn for (NC x)
2027 for dc_s in self.my_site.dsa_table.values():
2029 # If this partition NC (x) doesn't appear as a
2030 # replica (p) of NC (x) on the dsa DC (s) then
2032 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2035 # Pull out the NCReplica with the dn that
2036 # matches NC (x) we are examining.
2037 p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2039 # Replica (p) of NC (x) must be partial
2040 if not p_of_x.is_partial():
2043 # Replica (p) of NC (x) must satisfy the
2044 # "is present" criteria for DC (s) that
2046 if not p_of_x.is_present():
2049 # DC (s) must be a writable DSA other than
2050 # my DSA. In other words we'd only replicate
2051 # from other writable DSA
2052 if dc_s.is_ro() or dc_s is dc_local:
2055 # Certain replica graphs are produced only
2056 # for global catalogs, so test against
2057 # method input parameter
2058 if gc_only and not dc_s.is_gc():
2061 # If we haven't been told to turn off stale connection
2062 # detection and this dsa has a stale connection then
2064 if detect_stale and self.is_stale_link_connection(dc_s):
2067 # Replica meets criteria. Add it to table indexed
2068 # by the GUID of the DSA that it appears on
2069 r_list.append(p_of_x)
2071 # Append to R the NC replica that "should be present"
2073 r_list.append(l_of_x)
2075 r_list.sort(sort_replica_by_dsa_guid)
2078 max_node_edges = self.intrasite_max_node_edges(r_len)
2080 # Add a node for each r_list element to the replica graph
2083 node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2084 graph_list.append(node)
2086 # For each r(i) from (0 <= i < |R|-1)
2088 while i < (r_len-1):
2089 # Add an edge from r(i) to r(i+1) if r(i) is a full
2090 # replica or r(i+1) is a partial replica
2091 if not r_list[i].is_partial() or r_list[i+1].is_partial():
2092 graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
2094 # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2095 # replica or ri is a partial replica.
2096 if not r_list[i+1].is_partial() or r_list[i].is_partial():
2097 graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
2100 # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2101 # or r0 is a partial replica.
2102 if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
2103 graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
2105 # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2106 # r|R|-1 is a partial replica.
2107 if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
2108 graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
2110 DEBUG("r_list is length %s" % len(r_list))
2111 DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr))
2114 do_dot_files = self.dot_file_dir is not None and self.debug
2115 if self.verify or do_dot_files:
2117 dot_vertices = set()
2118 for v1 in graph_list:
2119 dot_vertices.add(v1.dsa_dnstr)
2120 for v2 in v1.edge_from:
2121 dot_edges.append((v2, v1.dsa_dnstr))
2122 dot_vertices.add(v2)
2124 verify_properties = ('connected',)
2125 verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2126 label='%s__%s__%s' % (site_local.site_dnstr,
2127 nctype_lut[nc_x.nc_type],
2129 properties=verify_properties, debug=DEBUG,
2131 dot_file_dir=self.dot_file_dir,
2134 rw_dot_vertices = set(x for x in dot_vertices
2135 if not self.get_dsa(x).is_ro())
2136 rw_dot_edges = [(a, b) for a, b in dot_edges if
2137 a in rw_dot_vertices and b in rw_dot_vertices]
2138 print rw_dot_edges, rw_dot_vertices
2139 rw_verify_properties = ('connected',
2140 'directed_double_ring_or_small')
2141 verify_and_dot('intrasite_rw_pre_ntdscon', rw_dot_edges,
2143 label='%s__%s__%s' % (site_local.site_dnstr,
2144 nctype_lut[nc_x.nc_type],
2146 properties=rw_verify_properties, debug=DEBUG,
2148 dot_file_dir=self.dot_file_dir,
2151 # For each existing nTDSConnection object implying an edge
2152 # from rj of R to ri such that j != i, an edge from rj to ri
2153 # is not already in the graph, and the total edges directed
2154 # to ri is less than n+2, the KCC adds that edge to the graph.
2155 for vertex in graph_list:
2156 dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2157 for connect in dsa.connect_table.values():
2158 remote = connect.from_dnstr
2159 if remote in self.my_site.dsa_table:
2160 vertex.add_edge_from(remote)
2162 DEBUG('reps are: %s' % ' '.join(x.rep_dsa_dnstr for x in r_list))
2163 DEBUG('dsas are: %s' % ' '.join(x.dsa_dnstr for x in graph_list))
2165 for tnode in graph_list:
2166 # To optimize replication latency in sites with many NC
2167 # replicas, the KCC adds new edges directed to ri to bring
2168 # the total edges to n+2, where the NC replica rk of R
2169 # from which the edge is directed is chosen at random such
2170 # that k != i and an edge from rk to ri is not already in
2173 # Note that the KCC tech ref does not give a number for
2174 # the definition of "sites with many NC replicas". At a
2175 # bare minimum to satisfy n+2 edges directed at a node we
2176 # have to have at least three replicas in |R| (i.e. if n
2177 # is zero then at least replicas from two other graph
2178 # nodes may direct edges to us).
2179 if r_len >= 3 and not tnode.has_sufficient_edges():
2180 candidates = [x for x in graph_list if
2182 x.dsa_dnstr not in tnode.edge_from)]
2184 debug.DEBUG_BLUE("looking for random link for %s. r_len %d, "
2185 "graph len %d candidates %d"
2186 % (tnode.dsa_dnstr, r_len, len(graph_list),
2189 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2191 while candidates and not tnode.has_sufficient_edges():
2192 other = random.choice(candidates)
2193 DEBUG("trying to add candidate %s" % other.dsa_dstr)
2194 if not tnode.add_edge_from(other):
2195 debug.DEBUG_RED("could not add %s" % other.dsa_dstr)
2196 candidates.remove(other)
2198 DEBUG_FN("not adding links to %s: nodes %s, links is %s/%s" %
2199 (tnode.dsa_dnstr, r_len, len(tnode.edge_from),
2202 # Print the graph node in debug mode
2203 DEBUG_FN("%s" % tnode)
2205 # For each edge directed to the local DC, ensure a nTDSConnection
2206 # points to us that satisfies the KCC criteria
2208 if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2209 tnode.add_connections_from_edges(dc_local, self.ip_transport)
2211 if self.verify or do_dot_files:
2213 dot_vertices = set()
2214 for v1 in graph_list:
2215 dot_vertices.add(v1.dsa_dnstr)
2216 for v2 in v1.edge_from:
2217 dot_edges.append((v2, v1.dsa_dnstr))
2218 dot_vertices.add(v2)
2220 verify_properties = ('connected',)
2221 verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2222 label='%s__%s__%s' % (site_local.site_dnstr,
2223 nctype_lut[nc_x.nc_type],
2225 properties=verify_properties, debug=DEBUG,
2227 dot_file_dir=self.dot_file_dir,
2230 rw_dot_vertices = set(x for x in dot_vertices
2231 if not self.get_dsa(x).is_ro())
2232 rw_dot_edges = [(a, b) for a, b in dot_edges if
2233 a in rw_dot_vertices and b in rw_dot_vertices]
2234 print rw_dot_edges, rw_dot_vertices
2235 rw_verify_properties = ('connected',
2236 'directed_double_ring_or_small')
2237 verify_and_dot('intrasite_rw_post_ntdscon', rw_dot_edges,
2239 label='%s__%s__%s' % (site_local.site_dnstr,
2240 nctype_lut[nc_x.nc_type],
2242 properties=rw_verify_properties, debug=DEBUG,
2244 dot_file_dir=self.dot_file_dir,
2247 def intrasite(self):
2248 """Generate the intrasite KCC connections
2250 As per MS-ADTS 6.2.2.2.
2252 If self.readonly is False, the connections are added to self.samdb.
2254 After this call, all DCs in each site with more than 3 DCs
2255 should be connected in a bidirectional ring. If a site has 2
2256 DCs, they will bidirectionally connected. Sites with many DCs
2257 may have arbitrary extra connections.
2263 DEBUG_FN("intrasite(): enter")
2265 # Test whether local site has topology disabled
2266 mysite = self.my_site
2267 if mysite.is_intrasite_topology_disabled():
2270 detect_stale = (not mysite.is_detect_stale_disabled())
2271 for connect in mydsa.connect_table.values():
2272 if connect.to_be_added:
2273 debug.DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2275 # Loop thru all the partitions, with gc_only False
2276 for partdn, part in self.part_table.items():
2277 self.construct_intrasite_graph(mysite, mydsa, part, False,
2279 for connect in mydsa.connect_table.values():
2280 if connect.to_be_added:
2281 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2283 # If the DC is a GC server, the KCC constructs an additional NC
2284 # replica graph (and creates nTDSConnection objects) for the
2285 # config NC as above, except that only NC replicas that "are present"
2286 # on GC servers are added to R.
2287 for connect in mydsa.connect_table.values():
2288 if connect.to_be_added:
2289 debug.DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2291 # Do it again, with gc_only True
2292 for partdn, part in self.part_table.items():
2293 if part.is_config():
2294 self.construct_intrasite_graph(mysite, mydsa, part, True,
2297 # The DC repeats the NC replica graph computation and nTDSConnection
2298 # creation for each of the NC replica graphs, this time assuming
2299 # that no DC has failed. It does so by re-executing the steps as
2300 # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2301 # set in the options attribute of the site settings object for
2302 # the local DC's site. (ie. we set "detec_stale" flag to False)
2303 for connect in mydsa.connect_table.values():
2304 if connect.to_be_added:
2305 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2307 # Loop thru all the partitions.
2308 for partdn, part in self.part_table.items():
2309 self.construct_intrasite_graph(mysite, mydsa, part, False,
2310 False) # don't detect stale
2312 # If the DC is a GC server, the KCC constructs an additional NC
2313 # replica graph (and creates nTDSConnection objects) for the
2314 # config NC as above, except that only NC replicas that "are present"
2315 # on GC servers are added to R.
2316 for connect in mydsa.connect_table.values():
2317 if connect.to_be_added:
2318 debug.DEBUG_RED("TO BE ADDED:\n%s" % connect)
2320 for partdn, part in self.part_table.items():
2321 if part.is_config():
2322 self.construct_intrasite_graph(mysite, mydsa, part, True,
2323 False) # don't detect stale
2325 self._commit_changes(mydsa)
2327 def list_dsas(self):
2328 """Compile a comprehensive list of DSA DNs
2330 These are all the DSAs on all the sites that KCC would be
2333 This method is not idempotent and may not work correctly in
2334 sequence with KCC.run().
2336 :return: a list of DSA DN strings.
2341 self.load_all_sites()
2342 self.load_all_partitions()
2343 self.load_ip_transport()
2344 self.load_all_sitelinks()
2346 for site in self.site_table.values():
2347 dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2348 for dsa in site.dsa_table.values()])
2351 def load_samdb(self, dburl, lp, creds):
2352 """Load the database using an url, loadparm, and credentials
2354 :param dburl: a database url.
2355 :param lp: a loadparm object.
2356 :param creds: a Credentials object.
2358 self.samdb = SamDB(url=dburl,
2359 session_info=system_session(),
2360 credentials=creds, lp=lp)
2362 def plot_all_connections(self, basename, verify_properties=()):
2363 """Helper function to plot and verify NTDSConnections
2365 :param basename: an identifying string to use in filenames and logs.
2366 :param verify_properties: properties to verify (default empty)
2368 verify = verify_properties and self.verify
2369 if not verify and self.dot_file_dir is None:
2377 for dsa in self.dsa_by_dnstr.values():
2378 dot_vertices.append(dsa.dsa_dnstr)
2380 vertex_colours.append('#cc0000')
2382 vertex_colours.append('#0000cc')
2383 for con in dsa.connect_table.values():
2384 if con.is_rodc_topology():
2385 edge_colours.append('red')
2387 edge_colours.append('blue')
2388 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2390 verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2391 label=self.my_dsa_dnstr,
2392 properties=verify_properties, debug=DEBUG,
2393 verify=verify, dot_file_dir=self.dot_file_dir,
2394 directed=True, edge_colors=edge_colours,
2395 vertex_colors=vertex_colours)
2397 def run(self, dburl, lp, creds, forced_local_dsa=None,
2398 forget_local_links=False, forget_intersite_links=False,
2399 attempt_live_connections=False):
2400 """Perform a KCC run, possibly updating repsFrom topology
2402 :param dburl: url of the database to work with.
2403 :param lp: a loadparm object.
2404 :param creds: a Credentials object.
2405 :param forced_local_dsa: pretend to be on the DSA with this dn_str
2406 :param forget_local_links: calculate as if no connections existed
2407 (boolean, default False)
2408 :param forget_intersite_links: calculate with only intrasite connection
2409 (boolean, default False)
2410 :param attempt_live_connections: attempt to connect to remote DSAs to
2411 determine link availability (boolean, default False)
2412 :return: 1 on error, 0 otherwise
2414 # We may already have a samdb setup if we are
2415 # currently importing an ldif for a test run
2416 if self.samdb is None:
2418 self.load_samdb(dburl, lp, creds)
2419 except ldb.LdbError, (num, msg):
2420 logger.error("Unable to open sam database %s : %s" %
2424 if forced_local_dsa:
2425 self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" %
2433 self.load_all_sites()
2434 self.load_all_partitions()
2435 self.load_ip_transport()
2436 self.load_all_sitelinks()
2438 if self.verify or self.dot_file_dir is not None:
2440 for site in self.site_table.values():
2441 guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2443 in site.dsa_table.items())
2445 self.plot_all_connections('dsa_initial')
2448 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2449 for dnstr, c_rep in current_reps.items():
2450 DEBUG("c_rep %s" % c_rep)
2451 dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2453 verify_and_dot('dsa_repsFrom_initial', dot_edges,
2454 directed=True, label=self.my_dsa_dnstr,
2455 properties=(), debug=DEBUG, verify=self.verify,
2456 dot_file_dir=self.dot_file_dir)
2459 for site in self.site_table.values():
2460 for dsa in site.dsa_table.values():
2461 current_reps, needed_reps = dsa.get_rep_tables()
2462 for dn_str, rep in current_reps.items():
2463 for reps_from in rep.rep_repsFrom:
2464 DEBUG("rep %s" % rep)
2465 dsa_guid = str(reps_from.source_dsa_obj_guid)
2466 dsa_dn = guid_to_dnstr[dsa_guid]
2467 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2469 verify_and_dot('dsa_repsFrom_initial_all', dot_edges,
2470 directed=True, label=self.my_dsa_dnstr,
2471 properties=(), debug=DEBUG, verify=self.verify,
2472 dot_file_dir=self.dot_file_dir)
2475 for link in self.sitelink_table.values():
2476 for a, b in itertools.combinations(link.site_list, 2):
2477 dot_edges.append((str(a), str(b)))
2478 properties = ('connected',)
2479 verify_and_dot('dsa_sitelink_initial', dot_edges,
2481 label=self.my_dsa_dnstr, properties=properties,
2482 debug=DEBUG, verify=self.verify,
2483 dot_file_dir=self.dot_file_dir)
2485 if forget_local_links:
2486 for dsa in self.my_site.dsa_table.values():
2487 dsa.connect_table = dict((k, v) for k, v in
2488 dsa.connect_table.items()
2489 if v.is_rodc_topology() or
2490 (v.from_dnstr not in
2491 self.my_site.dsa_table))
2492 self.plot_all_connections('dsa_forgotten_local')
2494 if forget_intersite_links:
2495 for site in self.site_table.values():
2496 for dsa in site.dsa_table.values():
2497 dsa.connect_table = dict((k, v) for k, v in
2498 dsa.connect_table.items()
2499 if site is self.my_site and
2500 v.is_rodc_topology())
2502 self.plot_all_connections('dsa_forgotten_all')
2504 if attempt_live_connections:
2505 # Encapsulates lp and creds in a function that
2506 # attempts connections to remote DSAs.
2507 def ping(self, dnsname):
2509 drs_utils.drsuapi_connect(dnsname, self.lp, self.creds)
2510 except drs_utils.drsException:
2515 # These are the published steps (in order) for the
2516 # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2519 self.refresh_failed_links_connections(ping)
2525 all_connected = self.intersite(ping)
2528 self.remove_unneeded_ntdsconn(all_connected)
2531 self.translate_ntdsconn()
2534 self.remove_unneeded_failed_links_connections()
2537 self.update_rodc_connection()
2539 if self.verify or self.dot_file_dir is not None:
2540 self.plot_all_connections('dsa_final',
2543 debug.DEBUG_MAGENTA("there are %d dsa guids" %
2548 my_dnstr = self.my_dsa.dsa_dnstr
2549 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2550 for dnstr, n_rep in needed_reps.items():
2551 for reps_from in n_rep.rep_repsFrom:
2552 guid_str = str(reps_from.source_dsa_obj_guid)
2553 dot_edges.append((my_dnstr, guid_to_dnstr[guid_str]))
2554 edge_colors.append('#' + str(n_rep.nc_guid)[:6])
2556 verify_and_dot('dsa_repsFrom_final', dot_edges, directed=True,
2557 label=self.my_dsa_dnstr,
2558 properties=(), debug=DEBUG, verify=self.verify,
2559 dot_file_dir=self.dot_file_dir,
2560 edge_colors=edge_colors)
2564 for site in self.site_table.values():
2565 for dsa in site.dsa_table.values():
2566 current_reps, needed_reps = dsa.get_rep_tables()
2567 for n_rep in needed_reps.values():
2568 for reps_from in n_rep.rep_repsFrom:
2569 dsa_guid = str(reps_from.source_dsa_obj_guid)
2570 dsa_dn = guid_to_dnstr[dsa_guid]
2571 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2573 verify_and_dot('dsa_repsFrom_final_all', dot_edges,
2574 directed=True, label=self.my_dsa_dnstr,
2575 properties=(), debug=DEBUG, verify=self.verify,
2576 dot_file_dir=self.dot_file_dir)
2583 def import_ldif(self, dburl, lp, creds, ldif_file, forced_local_dsa=None):
2584 """Import relevant objects and attributes from an LDIF file.
2586 The point of this function is to allow a programmer/debugger to
2587 import an LDIF file with non-security relevent information that
2588 was previously extracted from a DC database. The LDIF file is used
2589 to create a temporary abbreviated database. The KCC algorithm can
2590 then run against this abbreviated database for debug or test
2591 verification that the topology generated is computationally the
2592 same between different OSes and algorithms.
2594 :param dburl: path to the temporary abbreviated db to create
2595 :param lp: a loadparm object.
2596 :param cred: a Credentials object.
2597 :param ldif_file: path to the ldif file to import
2598 :param forced_local_dsa: perform KCC from this DSA's point of view
2599 :return: zero on success, 1 on error
2602 self.samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif_file,
2604 except ldif_import_export.LdifError, e:
2609 def export_ldif(self, dburl, lp, creds, ldif_file):
2610 """Save KCC relevant details to an ldif file
2612 The point of this function is to allow a programmer/debugger to
2613 extract an LDIF file with non-security relevent information from
2614 a DC database. The LDIF file can then be used to "import" via
2615 the import_ldif() function this file into a temporary abbreviated
2616 database. The KCC algorithm can then run against this abbreviated
2617 database for debug or test verification that the topology generated
2618 is computationally the same between different OSes and algorithms.
2620 :param dburl: LDAP database URL to extract info from
2621 :param lp: a loadparm object.
2622 :param cred: a Credentials object.
2623 :param ldif_file: output LDIF file name to create
2624 :return: zero on success, 1 on error
2627 ldif_import_export.samdb_to_ldif_file(self.samdb, dburl, lp, creds,
2629 except ldif_import_export.LdifError, e: