1 # define the KCC object
3 # Copyright (C) Dave Craft 2011
4 # Copyright (C) Andrew Bartlett 2015
6 # Andrew Bartlett's alleged work performed by his underlings Douglas
7 # Bagnall and Garming Sam.
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27 # ensure we get messages out immediately, so they get in the samba logs,
28 # and don't get swallowed by a timeout
29 os.environ['PYTHONUNBUFFERED'] = '1'
31 # forcing GMT avoids a problem in some timezones with kerberos. Both MIT
32 # heimdal can get mutual authentication errors due to the 24 second difference
33 # between UTC and GMT when using some zone files (eg. the PDT zone from
35 os.environ["TZ"] = "GMT"
37 # Find right directory when running from source tree
38 sys.path.insert(0, "bin/python")
45 from functools import partial
53 from samba.auth import system_session
54 from samba.samdb import SamDB
55 from samba.dcerpc import drsuapi
57 from samba.kcc.kcc_utils import Site, Partition, Transport, SiteLink
58 from samba.kcc.kcc_utils import NCReplica, NCType, nctype_lut, GraphNode
59 from samba.kcc.kcc_utils import RepsFromTo, KCCError, KCCFailedObject
60 from samba.kcc.kcc_utils import convert_schedule_to_repltimes
61 from samba.kcc.graph_utils import verify_and_dot
62 from samba import ldif_utils
64 from samba.kcc.graph import setup_graph, get_spanning_tree_edges
65 from samba.kcc.graph import Vertex
67 from samba.kcc.debug import DEBUG, DEBUG_FN, logger
68 from samba.kcc import debug
71 def sort_replica_by_dsa_guid(rep1, rep2):
72 """Helper to sort NCReplicas by their DSA guids
74 The guids need to be sorted in their NDR form.
76 :param rep1: An NC replica
77 :param rep2: Another replica
78 :return: -1, 0, or 1, indicating sort order.
80 return cmp(ndr_pack(rep1.rep_dsa_guid), ndr_pack(rep2.rep_dsa_guid))
83 def sort_dsa_by_gc_and_guid(dsa1, dsa2):
84 """Helper to sort DSAs by guid global catalog status
86 GC DSAs come before non-GC DSAs, other than that, the guids are
89 :param dsa1: A DSA object
90 :param dsa2: Another DSA
91 :return: -1, 0, or 1, indicating sort order.
93 if dsa1.is_gc() and not dsa2.is_gc():
95 if not dsa1.is_gc() and dsa2.is_gc():
97 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
100 def is_smtp_replication_available():
101 """Can the KCC use SMTP replication?
103 Currently always returns false because Samba doesn't implement
104 SMTP transfer for NC changes between DCs.
106 :return: Boolean (always False)
112 """The Knowledge Consistency Checker class.
114 A container for objects and methods allowing a run of the KCC. Produces a
115 set of connections in the samdb for which the Distributed Replication
116 Service can then utilize to replicate naming contexts
118 :param unix_now: The putative current time in seconds since 1970.
119 :param read_only: Don't write to the database.
120 :param verify: Check topological invariants for the generated graphs
121 :param debug: Write verbosely to stderr.
122 "param dot_files: write Graphviz files in /tmp showing topology
124 def __init__(self, unix_now, readonly=False, attempt_live_connections=True,
125 verify=False, debug=False, dot_files=False):
126 """Initializes the partitions class which can hold
127 our local DCs partitions or all the partitions in
130 self.part_table = {} # partition objects
132 self.transport_table = {}
133 self.ip_transport = None
134 self.sitelink_table = {}
135 self.dsa_by_dnstr = {}
136 self.dsa_by_guid = {}
138 self.get_dsa_by_guidstr = self.dsa_by_guid.get
139 self.get_dsa = self.dsa_by_dnstr.get
141 # TODO: These should be backed by a 'permanent' store so that when
142 # calling DRSGetReplInfo with DS_REPL_INFO_KCC_DSA_CONNECT_FAILURES,
143 # the failure information can be returned
144 self.kcc_failed_links = {}
145 self.kcc_failed_connections = set()
147 # Used in inter-site topology computation. A list
148 # of connections (by NTDSConnection object) that are
149 # to be kept when pruning un-needed NTDS Connections
150 self.kept_connections = set()
152 self.my_dsa_dnstr = None # My dsa DN
153 self.my_dsa = None # My dsa object
155 self.my_site_dnstr = None
160 self.unix_now = unix_now
161 self.nt_now = unix2nttime(unix_now)
162 self.readonly = readonly
163 self.attempt_live_connections = attempt_live_connections
166 self.dot_files = dot_files
168 def load_all_transports(self):
169 """Loads the inter-site transport objects for Sites
172 :raise KCCError: if no IP transport is 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=interSiteTransport)")
179 except ldb.LdbError, (enum, estr):
180 raise KCCError("Unable to find inter-site transports - (%s)" %
186 transport = Transport(dnstr)
188 transport.load_transport(self.samdb)
189 self.transport_table.setdefault(str(transport.guid),
191 if transport.name == 'IP':
192 self.ip_transport = transport
194 if self.ip_transport is None:
195 raise KCCError("there doesn't seem to be an IP transport")
197 def load_all_sitelinks(self):
198 """Loads the inter-site siteLink objects
201 :raise KCCError: if site-links aren't found
204 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
205 self.samdb.get_config_basedn(),
206 scope=ldb.SCOPE_SUBTREE,
207 expression="(objectClass=siteLink)")
208 except ldb.LdbError, (enum, estr):
209 raise KCCError("Unable to find inter-site siteLinks - (%s)" % estr)
215 if dnstr in self.sitelink_table:
218 sitelink = SiteLink(dnstr)
220 sitelink.load_sitelink(self.samdb)
222 # Assign this siteLink to table
224 self.sitelink_table[dnstr] = sitelink
226 def load_site(self, dn_str):
227 """Helper for load_my_site and load_all_sites.
229 Put all the site's DSAs into the KCC indices.
231 :param dn_str: a site dn_str
232 :return: the Site object pertaining to the dn_str
234 site = Site(dn_str, self.unix_now)
235 site.load_site(self.samdb)
237 # We avoid replacing the site with an identical copy in case
238 # somewhere else has a reference to the old one, which would
239 # lead to all manner of confusion and chaos.
240 guid = str(site.site_guid)
241 if guid not in self.site_table:
242 self.site_table[guid] = site
243 self.dsa_by_dnstr.update(site.dsa_table)
244 self.dsa_by_guid.update((str(x.dsa_guid), x)
245 for x in site.dsa_table.values())
247 return self.site_table[guid]
249 def load_my_site(self):
250 """Load the Site object for the local DSA.
254 self.my_site_dnstr = ("CN=%s,CN=Sites,%s" % (
255 self.samdb.server_site_name(),
256 self.samdb.get_config_basedn()))
258 self.my_site = self.load_site(self.my_site_dnstr)
260 def load_all_sites(self):
261 """Discover all sites and create Site objects.
264 :raise: KCCError if sites can't be found
267 res = self.samdb.search("CN=Sites,%s" %
268 self.samdb.get_config_basedn(),
269 scope=ldb.SCOPE_SUBTREE,
270 expression="(objectClass=site)")
271 except ldb.LdbError, (enum, estr):
272 raise KCCError("Unable to find sites - (%s)" % estr)
275 sitestr = str(msg.dn)
276 self.load_site(sitestr)
278 def load_my_dsa(self):
279 """Discover my nTDSDSA dn thru the rootDSE entry
282 :raise: KCCError if DSA can't be found
284 dn = ldb.Dn(self.samdb, "<GUID=%s>" % self.samdb.get_ntds_GUID())
286 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
287 attrs=["objectGUID"])
288 except ldb.LdbError, (enum, estr):
289 logger.warning("Search for %s failed: %s. This typically happens"
290 " in --importldif mode due to lack of module"
291 " support.", dn, estr)
293 # We work around the failure above by looking at the
294 # dsServiceName that was put in the fake rootdse by
295 # the --exportldif, rather than the
296 # samdb.get_ntds_GUID(). The disadvantage is that this
297 # mode requires we modify the @ROOTDSE dnq to support
299 service_name_res = self.samdb.search(base="",
300 scope=ldb.SCOPE_BASE,
301 attrs=["dsServiceName"])
302 dn = ldb.Dn(self.samdb,
303 service_name_res[0]["dsServiceName"][0])
305 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
306 attrs=["objectGUID"])
307 except ldb.LdbError, (enum, estr):
308 raise KCCError("Unable to find my nTDSDSA - (%s)" % estr)
311 raise KCCError("Unable to find my nTDSDSA at %s" %
314 ntds_guid = misc.GUID(self.samdb.get_ntds_GUID())
315 if misc.GUID(res[0]["objectGUID"][0]) != ntds_guid:
316 raise KCCError("Did not find the GUID we expected,"
317 " perhaps due to --importldif")
319 self.my_dsa_dnstr = str(res[0].dn)
321 self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
323 if self.my_dsa_dnstr not in self.dsa_by_dnstr:
324 debug.DEBUG_DARK_YELLOW("my_dsa %s isn't in self.dsas_by_dnstr:"
325 " it must be RODC.\n"
326 "Let's add it, because my_dsa is special!"
327 "\n(likewise for self.dsa_by_guid)" %
330 self.dsa_by_dnstr[self.my_dsa_dnstr] = self.my_dsa
331 self.dsa_by_guid[str(self.my_dsa.dsa_guid)] = self.my_dsa
333 def load_all_partitions(self):
334 """Discover and load all partitions.
336 Each NC is inserted into the part_table by partition
337 dn string (not the nCName dn string)
340 :raise: KCCError if partitions can't be found
343 res = self.samdb.search("CN=Partitions,%s" %
344 self.samdb.get_config_basedn(),
345 scope=ldb.SCOPE_SUBTREE,
346 expression="(objectClass=crossRef)")
347 except ldb.LdbError, (enum, estr):
348 raise KCCError("Unable to find partitions - (%s)" % estr)
351 partstr = str(msg.dn)
354 if partstr in self.part_table:
357 part = Partition(partstr)
359 part.load_partition(self.samdb)
360 self.part_table[partstr] = part
362 def should_be_present_test(self):
363 """Enumerate all loaded partitions and DSAs in local
364 site and test if NC should be present as replica
366 for partdn, part in self.part_table.items():
367 for dsadn, dsa in self.my_site.dsa_table.items():
368 needed, ro, partial = part.should_be_present(dsa)
369 logger.info("dsadn:%s\nncdn:%s\nneeded=%s:ro=%s:partial=%s\n" %
370 (dsadn, part.nc_dnstr, needed, ro, partial))
372 def refresh_failed_links_connections(self):
373 """Based on MS-ADTS 6.2.2.1"""
375 # Instead of NULL link with failure_count = 0, the tuple is
378 # LINKS: Refresh failed links
379 self.kcc_failed_links = {}
380 current, needed = self.my_dsa.get_rep_tables()
381 for replica in current.values():
382 # For every possible connection to replicate
383 for reps_from in replica.rep_repsFrom:
384 failure_count = reps_from.consecutive_sync_failures
385 if failure_count <= 0:
388 dsa_guid = str(reps_from.source_dsa_obj_guid)
389 time_first_failure = reps_from.last_success
390 last_result = reps_from.last_attempt
391 dns_name = reps_from.dns_name1
393 f = self.kcc_failed_links.get(dsa_guid)
395 f = KCCFailedObject(dsa_guid, failure_count,
396 time_first_failure, last_result,
398 self.kcc_failed_links[dsa_guid] = f
399 #elif f.failure_count == 0:
400 # f.failure_count = failure_count
401 # f.time_first_failure = time_first_failure
402 # f.last_result = last_result
404 f.failure_count = max(f.failure_count, failure_count)
405 f.time_first_failure = min(f.time_first_failure,
407 f.last_result = last_result
409 # CONNECTIONS: Refresh failed connections
410 restore_connections = set()
411 if self.attempt_live_connections:
412 DEBUG("refresh_failed_links: checking if links are still down")
413 for connection in self.kcc_failed_connections:
415 drs_utils.drsuapi_connect(connection.dns_name, lp, creds)
416 # Failed connection is no longer failing
417 restore_connections.add(connection)
418 except drs_utils.drsException:
419 # Failed connection still failing
420 connection.failure_count += 1
422 DEBUG("refresh_failed_links: not checking live links because we\n"
423 "weren't asked to --attempt-live-connections")
425 # Remove the restored connections from the failed connections
426 self.kcc_failed_connections.difference_update(restore_connections)
428 def is_stale_link_connection(self, target_dsa):
429 """Check whether a link to a remote DSA is stale
431 Used in MS-ADTS 6.2.2.2 Intrasite Connection Creation
433 Returns True if the remote seems to have been down for at
434 least two hours, otherwise False.
436 :param target_dsa: the remote DSA object
437 :return: True if link is stale, otherwise False
439 failed_link = self.kcc_failed_links.get(str(target_dsa.dsa_guid))
441 # failure_count should be > 0, but check anyways
442 if failed_link.failure_count > 0:
443 unix_first_failure = \
444 nttime2unix(failed_link.time_first_failure)
445 # TODO guard against future
446 if unix_first_failure > self.unix_now:
447 logger.error("The last success time attribute for \
448 repsFrom is in the future!")
450 # Perform calculation in seconds
451 if (self.unix_now - unix_first_failure) > 60 * 60 * 2:
458 # TODO: This should be backed by some form of local database
459 def remove_unneeded_failed_links_connections(self):
460 # Remove all tuples in kcc_failed_links where failure count = 0
461 # In this implementation, this should never happen.
463 # Remove all connections which were not used this run or connections
464 # that became active during this run.
467 def remove_unneeded_ntdsconn(self, all_connected):
468 """Remove unneeded NTDS Connections once topology is calculated
470 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections
472 :param all_connected: indicates whether all sites are connected
477 # New connections won't have GUIDs which are needed for
479 for cn_conn in mydsa.connect_table.values():
480 if cn_conn.guid is None:
482 cn_conn.guid = misc.GUID(str(uuid.uuid4()))
483 cn_conn.whenCreated = self.nt_now
485 cn_conn.load_connection(self.samdb)
487 for cn_conn in mydsa.connect_table.values():
489 s_dnstr = cn_conn.get_from_dnstr()
491 cn_conn.to_be_deleted = True
494 # Get the source DSA no matter what site
495 # XXX s_dsa is NEVER USED. It will be removed.
496 s_dsa = self.get_dsa(s_dnstr)
498 #XXX should an RODC be regarded as same site
499 same_site = s_dnstr in self.my_site.dsa_table
501 # Given an nTDSConnection object cn, if the DC with the
502 # nTDSDSA object dc that is the parent object of cn and
503 # the DC with the nTDSDA object referenced by cn!fromServer
504 # are in the same site, the KCC on dc deletes cn if all of
505 # the following are true:
507 # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
509 # No site settings object s exists for the local DC's site, or
510 # bit NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED is clear in
513 # Another nTDSConnection object cn2 exists such that cn and
514 # cn2 have the same parent object, cn!fromServer = cn2!fromServer,
517 # cn!whenCreated < cn2!whenCreated
519 # cn!whenCreated = cn2!whenCreated and
520 # cn!objectGUID < cn2!objectGUID
522 # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
524 if not cn_conn.is_generated():
527 if self.my_site.is_cleanup_ntdsconn_disabled():
530 # Loop thru connections looking for a duplicate that
531 # fulfills the previous criteria
533 packed_guid = ndr_pack(cn_conn.guid)
534 for cn2_conn in mydsa.connect_table.values():
535 if cn2_conn is cn_conn:
538 s2_dnstr = cn2_conn.get_from_dnstr()
540 # If the NTDS Connections has a different
541 # fromServer field then no match
542 if s2_dnstr != s_dnstr:
546 lesser = (cn_conn.whenCreated < cn2_conn.whenCreated or
547 (cn_conn.whenCreated == cn2_conn.whenCreated and
548 packed_guid < ndr_pack(cn2_conn.guid)))
553 if lesser and not cn_conn.is_rodc_topology():
554 cn_conn.to_be_deleted = True
556 # Given an nTDSConnection object cn, if the DC with the nTDSDSA
557 # object dc that is the parent object of cn and the DC with
558 # the nTDSDSA object referenced by cn!fromServer are in
559 # different sites, a KCC acting as an ISTG in dc's site
560 # deletes cn if all of the following are true:
562 # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
564 # cn!fromServer references an nTDSDSA object for a DC
565 # in a site other than the local DC's site.
567 # The keepConnections sequence returned by
568 # CreateIntersiteConnections() does not contain
569 # cn!objectGUID, or cn is "superseded by" (see below)
570 # another nTDSConnection cn2 and keepConnections
571 # contains cn2!objectGUID.
573 # The return value of CreateIntersiteConnections()
576 # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in
579 else: # different site
581 if not mydsa.is_istg():
584 if not cn_conn.is_generated():
588 # We are directly using this connection in intersite or
589 # we are using a connection which can supersede this one.
591 # MS-ADTS 6.2.2.4 - Removing Unnecessary Connections does not
592 # appear to be correct.
594 # 1. cn!fromServer and cn!parent appear inconsistent with
596 # 2. The repsFrom do not imply each other
598 if cn_conn in self.kept_connections: # and not_superceded:
601 # This is the result of create_intersite_connections
602 if not all_connected:
605 if not cn_conn.is_rodc_topology():
606 cn_conn.to_be_deleted = True
608 if mydsa.is_ro() or self.readonly:
609 for connect in mydsa.connect_table.values():
610 if connect.to_be_deleted:
611 DEBUG_FN("TO BE DELETED:\n%s" % connect)
612 if connect.to_be_added:
613 DEBUG_FN("TO BE ADDED:\n%s" % connect)
615 # Peform deletion from our tables but perform
616 # no database modification
617 mydsa.commit_connections(self.samdb, ro=True)
619 # Commit any modified connections
620 mydsa.commit_connections(self.samdb)
622 def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
623 """Update an repsFrom object if required.
625 Part of MS-ADTS 6.2.2.5.
627 Update t_repsFrom if necessary to satisfy requirements. Such
628 updates are typically required when the IDL_DRSGetNCChanges
629 server has moved from one site to another--for example, to
630 enable compression when the server is moved from the
631 client's site to another site.
633 The repsFrom.update_flags bit field may be modified
634 auto-magically if any changes are made here. See
635 kcc_utils.RepsFromTo for gory details.
638 :param n_rep: NC replica we need
639 :param t_repsFrom: repsFrom tuple to modify
640 :param s_rep: NC replica at source DSA
641 :param s_dsa: source DSA
642 :param cn_conn: Local DSA NTDSConnection child
646 s_dnstr = s_dsa.dsa_dnstr
649 same_site = s_dnstr in self.my_site.dsa_table
651 # if schedule doesn't match then update and modify
652 times = convert_schedule_to_repltimes(cn_conn.schedule)
653 if times != t_repsFrom.schedule:
654 t_repsFrom.schedule = times
655 update |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
657 # Bit DRS_PER_SYNC is set in replicaFlags if and only
658 # if nTDSConnection schedule has a value v that specifies
659 # scheduled replication is to be performed at least once
661 if cn_conn.is_schedule_minimum_once_per_week():
663 if ((t_repsFrom.replica_flags &
664 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0):
665 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
667 # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
668 # if the source DSA and the local DC's nTDSDSA object are
669 # in the same site or source dsa is the FSMO role owner
670 # of one or more FSMO roles in the NC replica.
671 if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
673 if ((t_repsFrom.replica_flags &
674 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0):
675 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
677 # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
678 # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
679 # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
680 # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
681 # t.replicaFlags if and only if s and the local DC's
682 # nTDSDSA object are in different sites.
683 if ((cn_conn.options &
684 dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0):
686 if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
689 # it LOOKS as if this next test is a bit silly: it
690 # checks the flag then sets it if it not set; the same
691 # effect could be achieved by unconditionally setting
692 # it. But in fact the repsFrom object has special
693 # magic attached to it, and altering replica_flags has
694 # side-effects. That is bad in my opinion, but there
696 if ((t_repsFrom.replica_flags &
697 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
698 t_repsFrom.replica_flags |= \
699 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
703 if ((t_repsFrom.replica_flags &
704 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
705 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
707 # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
708 # and only if s and the local DC's nTDSDSA object are
709 # not in the same site and the
710 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
711 # clear in cn!options
712 if (not same_site and
714 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
716 if ((t_repsFrom.replica_flags &
717 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0):
718 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
720 # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
721 # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
722 if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
724 if ((t_repsFrom.replica_flags &
725 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0):
726 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
728 # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
729 # set in t.replicaFlags if and only if cn!enabledConnection = false.
730 if not cn_conn.is_enabled():
732 if ((t_repsFrom.replica_flags &
733 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0):
734 t_repsFrom.replica_flags |= \
735 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
737 if ((t_repsFrom.replica_flags &
738 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0):
739 t_repsFrom.replica_flags |= \
740 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
742 # If s and the local DC's nTDSDSA object are in the same site,
743 # cn!transportType has no value, or the RDN of cn!transportType
746 # Bit DRS_MAIL_REP in t.replicaFlags is clear.
748 # t.uuidTransport = NULL GUID.
750 # t.uuidDsa = The GUID-based DNS name of s.
754 # Bit DRS_MAIL_REP in t.replicaFlags is set.
756 # If x is the object with dsname cn!transportType,
757 # t.uuidTransport = x!objectGUID.
759 # Let a be the attribute identified by
760 # x!transportAddressAttribute. If a is
761 # the dNSHostName attribute, t.uuidDsa = the GUID-based
762 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
764 # It appears that the first statement i.e.
766 # "If s and the local DC's nTDSDSA object are in the same
767 # site, cn!transportType has no value, or the RDN of
768 # cn!transportType is CN=IP:"
770 # could be a slightly tighter statement if it had an "or"
771 # between each condition. I believe this should
774 # IF (same-site) OR (no-value) OR (type-ip)
776 # because IP should be the primary transport mechanism
777 # (even in inter-site) and the absense of the transportType
778 # attribute should always imply IP no matter if its multi-site
780 # NOTE MS-TECH INCORRECT:
782 # All indications point to these statements above being
783 # incorrectly stated:
785 # t.uuidDsa = The GUID-based DNS name of s.
787 # Let a be the attribute identified by
788 # x!transportAddressAttribute. If a is
789 # the dNSHostName attribute, t.uuidDsa = the GUID-based
790 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
792 # because the uuidDSA is a GUID and not a GUID-base DNS
793 # name. Nor can uuidDsa hold (s!parent)!a if not
794 # dNSHostName. What should have been said is:
796 # t.naDsa = The GUID-based DNS name of s
798 # That would also be correct if transportAddressAttribute
799 # were "mailAddress" because (naDsa) can also correctly
800 # hold the SMTP ISM service address.
802 nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
804 # We're not currently supporting SMTP replication
805 # so is_smtp_replication_available() is currently
806 # always returning False
808 cn_conn.transport_dnstr is None or
809 cn_conn.transport_dnstr.find("CN=IP") == 0 or
810 not is_smtp_replication_available())):
812 if ((t_repsFrom.replica_flags &
813 drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0):
814 t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
816 t_repsFrom.transport_guid = misc.GUID()
818 # See (NOTE MS-TECH INCORRECT) above
819 if t_repsFrom.version == 0x1:
820 if t_repsFrom.dns_name1 is None or \
821 t_repsFrom.dns_name1 != nastr:
822 t_repsFrom.dns_name1 = nastr
824 if t_repsFrom.dns_name1 is None or \
825 t_repsFrom.dns_name2 is None or \
826 t_repsFrom.dns_name1 != nastr or \
827 t_repsFrom.dns_name2 != nastr:
828 t_repsFrom.dns_name1 = nastr
829 t_repsFrom.dns_name2 = nastr
832 # XXX This entire branch is NEVER used! Because we don't do SMTP!
833 # (see the if condition above). Just close your eyes here.
834 if ((t_repsFrom.replica_flags &
835 drsuapi.DRSUAPI_DRS_MAIL_REP) == 0x0):
836 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_MAIL_REP
838 # We have a transport type but its not an
839 # object in the database
840 if cn_conn.transport_guid not in self.transport_table:
841 raise KCCError("Missing inter-site transport - (%s)" %
842 cn_conn.transport_dnstr)
844 x_transport = self.transport_table[str(cn_conn.transport_guid)]
846 if t_repsFrom.transport_guid != x_transport.guid:
847 t_repsFrom.transport_guid = x_transport.guid
849 # See (NOTE MS-TECH INCORRECT) above
850 if x_transport.address_attr == "dNSHostName":
852 if t_repsFrom.version == 0x1:
853 if t_repsFrom.dns_name1 is None or \
854 t_repsFrom.dns_name1 != nastr:
855 t_repsFrom.dns_name1 = nastr
857 if t_repsFrom.dns_name1 is None or \
858 t_repsFrom.dns_name2 is None or \
859 t_repsFrom.dns_name1 != nastr or \
860 t_repsFrom.dns_name2 != nastr:
861 t_repsFrom.dns_name1 = nastr
862 t_repsFrom.dns_name2 = nastr
865 # MS tech specification says we retrieve the named
866 # attribute in "transportAddressAttribute" from the parent of
869 pdnstr = s_dsa.get_parent_dnstr()
870 attrs = [x_transport.address_attr]
872 res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
874 except ldb.LdbError, (enum, estr):
876 "Unable to find attr (%s) for (%s) - (%s)" %
877 (x_transport.address_attr, pdnstr, estr))
880 nastr = str(msg[x_transport.address_attr][0])
882 # See (NOTE MS-TECH INCORRECT) above
883 if t_repsFrom.version == 0x1:
884 if t_repsFrom.dns_name1 is None or \
885 t_repsFrom.dns_name1 != nastr:
886 t_repsFrom.dns_name1 = nastr
888 if t_repsFrom.dns_name1 is None or \
889 t_repsFrom.dns_name2 is None or \
890 t_repsFrom.dns_name1 != nastr or \
891 t_repsFrom.dns_name2 != nastr:
893 t_repsFrom.dns_name1 = nastr
894 t_repsFrom.dns_name2 = nastr
896 if t_repsFrom.is_modified():
897 DEBUG_FN("modify_repsFrom(): %s" % t_repsFrom)
899 def is_repsFrom_implied(self, n_rep, cn_conn):
900 """Given a NC replica and NTDS Connection, determine if the connection
901 implies a repsFrom tuple should be present from the source DSA listed
902 in the connection to the naming context
904 :param n_rep: NC replica
905 :param conn: NTDS Connection
906 ::returns (True || False), source DSA:
908 #XXX different conditions for "implies" than MS-ADTS 6.2.2
910 # NTDS Connection must satisfy all the following criteria
911 # to imply a repsFrom tuple is needed:
913 # cn!enabledConnection = true.
914 # cn!options does not contain NTDSCONN_OPT_RODC_TOPOLOGY.
915 # cn!fromServer references an nTDSDSA object.
919 if cn_conn.is_enabled() and not cn_conn.is_rodc_topology():
920 s_dnstr = cn_conn.get_from_dnstr()
921 if s_dnstr is not None:
922 s_dsa = self.get_dsa(s_dnstr)
924 # No DSA matching this source DN string?
928 # To imply a repsFrom tuple is needed, each of these
931 # An NC replica of the NC "is present" on the DC to
932 # which the nTDSDSA object referenced by cn!fromServer
935 # An NC replica of the NC "should be present" on
937 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
939 if s_rep is None or not s_rep.is_present():
942 # To imply a repsFrom tuple is needed, each of these
945 # The NC replica on the DC referenced by cn!fromServer is
946 # a writable replica or the NC replica that "should be
947 # present" on the local DC is a partial replica.
949 # The NC is not a domain NC, the NC replica that
950 # "should be present" on the local DC is a partial
951 # replica, cn!transportType has no value, or
952 # cn!transportType has an RDN of CN=IP.
954 implied = (not s_rep.is_ro() or n_rep.is_partial()) and \
955 (not n_rep.is_domain() or
956 n_rep.is_partial() or
957 cn_conn.transport_dnstr is None or
958 cn_conn.transport_dnstr.find("CN=IP") == 0)
965 def translate_ntdsconn(self, current_dsa=None):
966 """Adjust repsFrom to match NTDSConnections
968 This function adjusts values of repsFrom abstract attributes of NC
969 replicas on the local DC to match those implied by
970 nTDSConnection objects.
972 Based on [MS-ADTS] 6.2.2.5
974 :param current_dsa: optional DSA on whose behalf we are acting.
979 if current_dsa is None:
980 current_dsa = self.my_dsa
982 if current_dsa.is_translate_ntdsconn_disabled():
983 DEBUG_FN("skipping translate_ntdsconn() "
984 "because disabling flag is set")
987 DEBUG_FN("translate_ntdsconn(): enter")
989 current_rep_table, needed_rep_table = current_dsa.get_rep_tables()
991 # Filled in with replicas we currently have that need deleting
994 # We're using the MS notation names here to allow
995 # correlation back to the published algorithm.
997 # n_rep - NC replica (n)
998 # t_repsFrom - tuple (t) in n!repsFrom
999 # s_dsa - Source DSA of the replica. Defined as nTDSDSA
1000 # object (s) such that (s!objectGUID = t.uuidDsa)
1001 # In our IDL representation of repsFrom the (uuidDsa)
1002 # attribute is called (source_dsa_obj_guid)
1003 # cn_conn - (cn) is nTDSConnection object and child of the local
1004 # DC's nTDSDSA object and (cn!fromServer = s)
1005 # s_rep - source DSA replica of n
1007 # If we have the replica and its not needed
1008 # then we add it to the "to be deleted" list.
1009 for dnstr in current_rep_table:
1010 if dnstr not in needed_rep_table:
1011 delete_reps.add(dnstr)
1013 DEBUG_FN('current %d needed %d delete %d' % (len(current_rep_table),
1014 len(needed_rep_table), len(delete_reps)))
1017 DEBUG('deleting these reps: %s' % delete_reps)
1018 for dnstr in delete_reps:
1019 del current_rep_table[dnstr]
1021 # Now perform the scan of replicas we'll need
1022 # and compare any current repsFrom against the
1024 for n_rep in needed_rep_table.values():
1026 # load any repsFrom and fsmo roles as we'll
1027 # need them during connection translation
1028 n_rep.load_repsFrom(self.samdb)
1029 n_rep.load_fsmo_roles(self.samdb)
1031 # Loop thru the existing repsFrom tupples (if any)
1032 # XXX This is a list and could contain duplicates
1033 # (multiple load_repsFrom calls)
1034 for t_repsFrom in n_rep.rep_repsFrom:
1036 # for each tuple t in n!repsFrom, let s be the nTDSDSA
1037 # object such that s!objectGUID = t.uuidDsa
1038 guidstr = str(t_repsFrom.source_dsa_obj_guid)
1039 s_dsa = self.get_dsa_by_guidstr(guidstr)
1041 # Source dsa is gone from config (strange)
1042 # so cleanup stale repsFrom for unlisted DSA
1044 logger.warning("repsFrom source DSA guid (%s) not found" %
1046 t_repsFrom.to_be_deleted = True
1049 s_dnstr = s_dsa.dsa_dnstr
1051 # Retrieve my DSAs connection object (if it exists)
1052 # that specifies the fromServer equivalent to
1053 # the DSA that is specified in the repsFrom source
1054 connections = current_dsa.get_connection_by_from_dnstr(s_dnstr)
1059 for con in connections:
1060 if con.is_rodc_topology():
1064 # Let (cn) be the nTDSConnection object such that (cn)
1065 # is a child of the local DC's nTDSDSA object and
1066 # (cn!fromServer = s) and (cn!options) does not contain
1067 # NTDSCONN_OPT_RODC_TOPOLOGY or NULL if no such (cn) exists.
1069 # KCC removes this repsFrom tuple if any of the following
1074 #XXX varying possible interpretations of rodc_topology
1076 t_repsFrom.to_be_deleted = True
1079 # [...] KCC removes this repsFrom tuple if:
1081 # No NC replica of the NC "is present" on DSA that
1082 # would be source of replica
1084 # A writable replica of the NC "should be present" on
1085 # the local DC, but a partial replica "is present" on
1087 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
1089 if s_rep is None or not s_rep.is_present() or \
1090 (not n_rep.is_ro() and s_rep.is_partial()):
1092 t_repsFrom.to_be_deleted = True
1095 # If the KCC did not remove t from n!repsFrom, it updates t
1096 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
1098 # Loop thru connections and add implied repsFrom tuples
1099 # for each NTDSConnection under our local DSA if the
1100 # repsFrom is not already present
1101 for cn_conn in current_dsa.connect_table.values():
1103 implied, s_dsa = self.is_repsFrom_implied(n_rep, cn_conn)
1107 # Loop thru the existing repsFrom tupples (if any) and
1108 # if we already have a tuple for this connection then
1109 # no need to proceed to add. It will have been changed
1110 # to have the correct attributes above
1111 for t_repsFrom in n_rep.rep_repsFrom:
1112 guidstr = str(t_repsFrom.source_dsa_obj_guid)
1114 if s_dsa is self.get_dsa_by_guidstr(guidstr):
1121 # Create a new RepsFromTo and proceed to modify
1122 # it according to specification
1123 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
1125 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
1127 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
1129 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
1131 # Add to our NC repsFrom as this is newly computed
1132 if t_repsFrom.is_modified():
1133 n_rep.rep_repsFrom.append(t_repsFrom)
1136 # Display any to be deleted or modified repsFrom
1137 text = n_rep.dumpstr_to_be_deleted()
1139 logger.info("TO BE DELETED:\n%s" % text)
1140 text = n_rep.dumpstr_to_be_modified()
1142 logger.info("TO BE MODIFIED:\n%s" % text)
1144 # Peform deletion from our tables but perform
1145 # no database modification
1146 n_rep.commit_repsFrom(self.samdb, ro=True)
1148 # Commit any modified repsFrom to the NC replica
1149 n_rep.commit_repsFrom(self.samdb)
1151 def merge_failed_links(self):
1152 """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
1154 The KCC on a writable DC attempts to merge the link and connection
1155 failure information from bridgehead DCs in its own site to help it
1156 identify failed bridgehead DCs.
1158 Based on MS-ADTS 6.2.2.3.2 "Merge of kCCFailedLinks and kCCFailedLinks
1161 :param ping: An oracle of current bridgehead availability
1164 # 1. Queries every bridgehead server in your site (other than yourself)
1165 # 2. For every ntDSConnection that references a server in a different
1166 # site merge all the failure info
1168 # XXX - not implemented yet
1169 if self.attempt_live_connections:
1170 debug.DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1172 DEBUG_FN("skipping merge_failed_links() because it requires "
1173 "real network connections\n"
1174 "and we weren't asked to --attempt-live-connections")
1176 def setup_graph(self, part):
1177 """Set up an intersite graph
1179 An intersite graph has a Vertex for each site object, a
1180 MultiEdge for each SiteLink object, and a MutliEdgeSet for
1181 each siteLinkBridge object (or implied siteLinkBridge). It
1182 reflects the intersite topology in a slightly more abstract
1185 Roughly corresponds to MS-ADTS 6.2.2.3.4.3
1187 :param part: a Partition object
1188 :returns: an InterSiteGraph object
1190 # If 'Bridge all site links' is enabled and Win2k3 bridges required
1192 # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1193 # No documentation for this however, ntdsapi.h appears to have:
1194 # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1195 bridges_required = self.my_site.site_options & 0x00001002 == 0
1197 g = setup_graph(part, self.site_table, self.transport_table,
1198 self.sitelink_table, bridges_required)
1201 for edge in g.edges:
1202 for a, b in itertools.combinations(edge.vertices, 2):
1203 dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1204 verify_properties = ()
1205 verify_and_dot('site_edges', dot_edges, directed=False,
1206 label=self.my_dsa_dnstr,
1207 properties=verify_properties, debug=DEBUG,
1209 dot_files=self.dot_files)
1213 def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1214 """Get a bridghead DC for a site.
1216 Part of MS-ADTS 6.2.2.3.4.4
1218 :param site: site object representing for which a bridgehead
1220 :param part: crossRef for NC to replicate.
1221 :param transport: interSiteTransport object for replication
1223 :param partial_ok: True if a DC containing a partial
1224 replica or a full replica will suffice, False if only
1225 a full replica will suffice.
1226 :param detect_failed: True to detect failed DCs and route
1227 replication traffic around them, False to assume no DC
1229 :return: dsa object for the bridgehead DC or None
1232 bhs = self.get_all_bridgeheads(site, part, transport,
1233 partial_ok, detect_failed)
1235 debug.DEBUG_MAGENTA("get_bridgehead:\n\tsitedn=%s\n\tbhdn=None" %
1239 debug.DEBUG_GREEN("get_bridgehead:\n\tsitedn=%s\n\tbhdn=%s" %
1240 (site.site_dnstr, bhs[0].dsa_dnstr))
1243 def get_all_bridgeheads(self, site, part, transport,
1244 partial_ok, detect_failed):
1245 """Get all bridghead DCs on a site satisfying the given criteria
1247 Part of MS-ADTS 6.2.2.3.4.4
1249 :param site: site object representing the site for which
1250 bridgehead DCs are desired.
1251 :param part: partition for NC to replicate.
1252 :param transport: interSiteTransport object for
1253 replication traffic.
1254 :param partial_ok: True if a DC containing a partial
1255 replica or a full replica will suffice, False if
1256 only a full replica will suffice.
1257 :param detect_failed: True to detect failed DCs and route
1258 replication traffic around them, FALSE to assume
1260 :return: list of dsa object for available bridgehead DCs
1265 DEBUG_FN("get_all_bridgeheads: %s" % transport.name)
1266 if 'Site-5' in site.site_dnstr:
1267 debug.DEBUG_RED("get_all_bridgeheads with %s, part%s, "
1268 "partial_ok %s detect_failed %s" %
1269 (site.site_dnstr, part.partstr, partial_ok,
1271 DEBUG_FN(site.rw_dsa_table)
1272 for dsa in site.rw_dsa_table.values():
1274 pdnstr = dsa.get_parent_dnstr()
1276 # IF t!bridgeheadServerListBL has one or more values and
1277 # t!bridgeheadServerListBL does not contain a reference
1278 # to the parent object of dc then skip dc
1279 if ((len(transport.bridgehead_list) != 0 and
1280 pdnstr not in transport.bridgehead_list)):
1283 # IF dc is in the same site as the local DC
1284 # IF a replica of cr!nCName is not in the set of NC replicas
1285 # that "should be present" on dc or a partial replica of the
1286 # NC "should be present" but partialReplicasOkay = FALSE
1288 if self.my_site.same_site(dsa):
1289 needed, ro, partial = part.should_be_present(dsa)
1290 if not needed or (partial and not partial_ok):
1292 rep = dsa.get_current_replica(part.nc_dnstr)
1295 # IF an NC replica of cr!nCName is not in the set of NC
1296 # replicas that "are present" on dc or a partial replica of
1297 # the NC "is present" but partialReplicasOkay = FALSE
1300 rep = dsa.get_current_replica(part.nc_dnstr)
1301 if rep is None or (rep.is_partial() and not partial_ok):
1304 # IF AmIRODC() and cr!nCName corresponds to default NC then
1305 # Let dsaobj be the nTDSDSA object of the dc
1306 # IF dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1308 if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1309 if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1312 # IF t!name != "IP" and the parent object of dc has no value for
1313 # the attribute specified by t!transportAddressAttribute
1315 if transport.name != "IP":
1316 # MS tech specification says we retrieve the named
1317 # attribute in "transportAddressAttribute" from the parent
1320 attrs = [transport.address_attr]
1322 res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
1324 except ldb.LdbError, (enum, estr):
1328 if transport.address_attr not in msg:
1330 #XXX nastr is NEVER USED. It will be removed.
1331 nastr = str(msg[transport.address_attr][0])
1333 # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1335 if self.is_bridgehead_failed(dsa, detect_failed):
1336 DEBUG("bridgehead is failed")
1339 DEBUG_FN("get_all_bridgeheads: dsadn=%s" % dsa.dsa_dnstr)
1342 # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1344 # SORT bhs such that all GC servers precede DCs that are not GC
1345 # servers, and otherwise by ascending objectGUID
1347 # SORT bhs in a random order
1348 if site.is_random_bridgehead_disabled():
1349 bhs.sort(sort_dsa_by_gc_and_guid)
1352 debug.DEBUG_YELLOW(bhs)
1355 def is_bridgehead_failed(self, dsa, detect_failed):
1356 """Determine whether a given DC is known to be in a failed state
1358 :param dsa: the bridgehead to test
1359 :param detect_failed: True to really check, False to assume no failure
1360 :return: True if and only if the DC should be considered failed
1362 Here we DEPART from the pseudo code spec which appears to be
1363 wrong. It says, in full:
1365 /***** BridgeheadDCFailed *****/
1366 /* Determine whether a given DC is known to be in a failed state.
1367 * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1368 * IN: detectFailedDCs - TRUE if and only failed DC detection is
1370 * RETURNS: TRUE if and only if the DC should be considered to be in a
1373 BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1375 IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1376 the options attribute of the site settings object for the local
1379 ELSEIF a tuple z exists in the kCCFailedLinks or
1380 kCCFailedConnections variables such that z.UUIDDsa =
1381 objectGUID, z.FailureCount > 1, and the current time -
1382 z.TimeFirstFailure > 2 hours
1385 RETURN detectFailedDCs
1389 where you will see detectFailedDCs is not behaving as
1390 advertised -- it is acting as a default return code in the
1391 event that a failure is not detected, not a switch turning
1392 detection on or off. Elsewhere the documentation seems to
1393 concur with the comment rather than the code.
1395 if not detect_failed:
1398 # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1399 # When DETECT_STALE_DISABLED, we can never know of if
1400 # it's in a failed state
1401 if self.my_site.site_options & 0x00000008:
1404 return self.is_stale_link_connection(dsa)
1406 def create_connection(self, part, rbh, rsite, transport,
1407 lbh, lsite, link_opt, link_sched,
1408 partial_ok, detect_failed):
1409 """Create an nTDSConnection object as specified if it doesn't exist.
1411 Part of MS-ADTS 6.2.2.3.4.5
1413 :param part: crossRef object for the NC to replicate.
1414 :param rbh: nTDSDSA object for DC to act as the
1415 IDL_DRSGetNCChanges server (which is in a site other
1416 than the local DC's site).
1417 :param rsite: site of the rbh
1418 :param transport: interSiteTransport object for the transport
1419 to use for replication traffic.
1420 :param lbh: nTDSDSA object for DC to act as the
1421 IDL_DRSGetNCChanges client (which is in the local DC's site).
1422 :param lsite: site of the lbh
1423 :param link_opt: Replication parameters (aggregated siteLink options,
1425 :param link_sched: Schedule specifying the times at which
1426 to begin replicating.
1427 :partial_ok: True if bridgehead DCs containing partial
1428 replicas of the NC are acceptable.
1429 :param detect_failed: True to detect failed DCs and route
1430 replication traffic around them, FALSE to assume no DC
1433 rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1435 rbh_table = {x.dsa_dnstr: x for x in rbhs_all}
1437 debug.DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all),
1438 [x.dsa_dnstr for x in rbhs_all]))
1440 # MS-TECH says to compute rbhs_avail but then doesn't use it
1441 # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1442 # partial_ok, detect_failed)
1444 lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1447 lbhs_all.append(lbh)
1449 debug.DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all),
1450 [x.dsa_dnstr for x in lbhs_all]))
1452 # MS-TECH says to compute lbhs_avail but then doesn't use it
1453 # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1454 # partial_ok, detect_failed)
1456 # FOR each nTDSConnection object cn such that the parent of cn is
1457 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1458 for ldsa in lbhs_all:
1459 for cn in ldsa.connect_table.values():
1461 rdsa = rbh_table.get(cn.from_dnstr)
1465 debug.DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1466 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1467 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1468 # cn!transportType references t
1469 if ((cn.is_generated() and
1470 not cn.is_rodc_topology() and
1471 cn.transport_guid == transport.guid)):
1473 # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1474 # cn!options and cn!schedule != sch
1475 # Perform an originating update to set cn!schedule to
1477 if ((not cn.is_user_owned_schedule() and
1478 not cn.is_equivalent_schedule(link_sched))):
1479 cn.schedule = link_sched
1480 cn.set_modified(True)
1482 # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1483 # NTDSCONN_OPT_USE_NOTIFY are set in cn
1484 if cn.is_override_notify_default() and \
1487 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1489 # Perform an originating update to clear bits
1490 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1491 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1492 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1494 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1495 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1496 cn.set_modified(True)
1501 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1503 # Perform an originating update to set bits
1504 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1505 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1506 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1508 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1509 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1510 cn.set_modified(True)
1512 # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1513 if cn.is_twoway_sync():
1515 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1517 # Perform an originating update to clear bit
1518 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1519 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1520 cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1521 cn.set_modified(True)
1526 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1528 # Perform an originating update to set bit
1529 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1530 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1531 cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1532 cn.set_modified(True)
1534 # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1536 if cn.is_intersite_compression_disabled():
1538 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1540 # Perform an originating update to clear bit
1541 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1544 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0):
1546 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1547 cn.set_modified(True)
1551 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1553 # Perform an originating update to set bit
1554 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1557 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1559 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1560 cn.set_modified(True)
1562 # Display any modified connection
1564 if cn.to_be_modified:
1565 logger.info("TO BE MODIFIED:\n%s" % cn)
1567 ldsa.commit_connections(self.samdb, ro=True)
1569 ldsa.commit_connections(self.samdb)
1572 valid_connections = 0
1574 # FOR each nTDSConnection object cn such that cn!parent is
1575 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1576 for ldsa in lbhs_all:
1577 for cn in ldsa.connect_table.values():
1579 rdsa = rbh_table.get(cn.from_dnstr)
1583 debug.DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1585 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1586 # cn!transportType references t) and
1587 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1588 if (((not cn.is_generated() or
1589 cn.transport_guid == transport.guid) and
1590 not cn.is_rodc_topology())):
1592 # LET rguid be the objectGUID of the nTDSDSA object
1593 # referenced by cn!fromServer
1594 # LET lguid be (cn!parent)!objectGUID
1596 # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1597 # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1598 # Increment cValidConnections by 1
1599 if ((not self.is_bridgehead_failed(rdsa, detect_failed) and
1600 not self.is_bridgehead_failed(ldsa, detect_failed))):
1601 valid_connections += 1
1603 # IF keepConnections does not contain cn!objectGUID
1604 # APPEND cn!objectGUID to keepConnections
1605 self.kept_connections.add(cn)
1608 debug.DEBUG_RED("valid connections %d" % valid_connections)
1609 DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1610 # IF cValidConnections = 0
1611 if valid_connections == 0:
1613 # LET opt be NTDSCONN_OPT_IS_GENERATED
1614 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1616 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1617 # SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1618 # NTDSCONN_OPT_USE_NOTIFY in opt
1619 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1620 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1621 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1623 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1624 # SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1625 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1626 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1628 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1630 # SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1632 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1633 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1635 # Perform an originating update to create a new nTDSConnection
1636 # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1637 # cn!options = opt, cn!transportType is a reference to t,
1638 # cn!fromServer is a reference to rbh, and cn!schedule = sch
1639 DEBUG_FN("new connection, KCC dsa: %s" % self.my_dsa.dsa_dnstr)
1640 cn = lbh.new_connection(opt, 0, transport,
1641 rbh.dsa_dnstr, link_sched)
1643 # Display any added connection
1646 logger.info("TO BE ADDED:\n%s" % cn)
1648 lbh.commit_connections(self.samdb, ro=True)
1650 lbh.commit_connections(self.samdb)
1652 # APPEND cn!objectGUID to keepConnections
1653 self.kept_connections.add(cn)
1655 def add_transports(self, vertex, local_vertex, graph, detect_failed):
1656 """Build a Vertex's transport lists
1658 Each vertex has accept_red_red and accept_black lists that
1659 list what transports they accept under various conditions. The
1660 only transport that is ever accepted is IP, and a dummy extra
1661 transport called "EDGE_TYPE_ALL".
1663 Part of MS-ADTS 6.2.2.3.4.3 -- ColorVertices
1665 :param vertex: the remote vertex we are thinking about
1666 :param local_vertex: the vertex relating to the local site.
1667 :param graph: the intersite graph
1668 :param detect_failed: whether to detect failed links
1669 :return: True if some bridgeheads were not found
1671 # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1672 # here, but using vertex seems to make more sense. That is,
1673 # the docs want this:
1675 #bh = self.get_bridgehead(vertex.site, vertex.part, transport,
1676 # local_vertex.is_black(), detect_failed)
1680 vertex.accept_red_red = []
1681 vertex.accept_black = []
1682 found_failed = False
1683 for t_guid, transport in self.transport_table.items():
1684 if transport.name != 'IP':
1685 #XXX well this is cheating a bit
1686 logger.warning("WARNING: we are ignoring a transport named %r"
1690 # FLAG_CR_NTDS_DOMAIN 0x00000002
1691 if ((vertex.is_red() and transport.name != "IP" and
1692 vertex.part.system_flags & 0x00000002)):
1695 if vertex not in graph.connected_vertices:
1698 partial_replica_okay = vertex.is_black()
1699 bh = self.get_bridgehead(vertex.site, vertex.part, transport,
1700 partial_replica_okay, detect_failed)
1705 vertex.accept_red_red.append(t_guid)
1706 vertex.accept_black.append(t_guid)
1708 # Add additional transport to allow another run of Dijkstra
1709 vertex.accept_red_red.append("EDGE_TYPE_ALL")
1710 vertex.accept_black.append("EDGE_TYPE_ALL")
1714 def create_connections(self, graph, part, detect_failed):
1715 """Construct an NC replica graph for the NC identified by
1716 the given crossRef, then create any additional nTDSConnection
1719 :param graph: site graph.
1720 :param part: crossRef object for NC.
1721 :param detect_failed: True to detect failed DCs and route
1722 replication traffic around them, False to assume no DC
1725 Modifies self.kept_connections by adding any connections
1726 deemed to be "in use".
1728 ::returns: (all_connected, found_failed_dc)
1729 (all_connected) True if the resulting NC replica graph
1730 connects all sites that need to be connected.
1731 (found_failed_dc) True if one or more failed DCs were
1734 all_connected = True
1735 found_failed = False
1737 DEBUG_FN("create_connections(): enter\n"
1738 "\tpartdn=%s\n\tdetect_failed=%s" %
1739 (part.nc_dnstr, detect_failed))
1741 # XXX - This is a highly abbreviated function from the MS-TECH
1742 # ref. It creates connections between bridgeheads to all
1743 # sites that have appropriate replicas. Thus we are not
1744 # creating a minimum cost spanning tree but instead
1745 # producing a fully connected tree. This should produce
1746 # a full (albeit not optimal cost) replication topology.
1748 my_vertex = Vertex(self.my_site, part)
1749 my_vertex.color_vertex()
1751 for v in graph.vertices:
1753 if self.add_transports(v, my_vertex, graph, False):
1756 # No NC replicas for this NC in the site of the local DC,
1757 # so no nTDSConnection objects need be created
1758 if my_vertex.is_white():
1759 return all_connected, found_failed
1761 edge_list, n_components = get_spanning_tree_edges(graph,
1765 DEBUG_FN("%s Number of components: %d" %
1766 (part.nc_dnstr, n_components))
1767 if n_components > 1:
1768 all_connected = False
1770 # LET partialReplicaOkay be TRUE if and only if
1771 # localSiteVertex.Color = COLOR.BLACK
1772 partial_ok = my_vertex.is_black()
1774 # Utilize the IP transport only for now
1775 transport = self.ip_transport
1777 DEBUG("edge_list %s" % edge_list)
1779 # XXX more accurate comparison?
1780 if e.directed and e.vertices[0].site is self.my_site:
1783 if e.vertices[0].site is self.my_site:
1784 rsite = e.vertices[1].site
1786 rsite = e.vertices[0].site
1788 # We don't make connections to our own site as that
1789 # is intrasite topology generator's job
1790 if rsite is self.my_site:
1791 DEBUG("rsite is my_site")
1794 # Determine bridgehead server in remote site
1795 rbh = self.get_bridgehead(rsite, part, transport,
1796 partial_ok, detect_failed)
1800 # RODC acts as an BH for itself
1802 # LET lbh be the nTDSDSA object of the local DC
1804 # LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1805 # cr, t, partialReplicaOkay, detectFailedDCs)
1806 if self.my_dsa.is_ro():
1807 lsite = self.my_site
1810 lsite = self.my_site
1811 lbh = self.get_bridgehead(lsite, part, transport,
1812 partial_ok, detect_failed)
1815 debug.DEBUG_RED("DISASTER! lbh is None")
1818 debug.DEBUG_CYAN("SITES")
1820 debug.DEBUG_BLUE("vertices")
1822 debug.DEBUG_BLUE("bridgeheads")
1824 debug.DEBUG_BLUE("-" * 70)
1826 sitelink = e.site_link
1827 if sitelink is None:
1831 link_opt = sitelink.options
1832 link_sched = sitelink.schedule
1834 self.create_connection(part, rbh, rsite, transport,
1835 lbh, lsite, link_opt, link_sched,
1836 partial_ok, detect_failed)
1838 return all_connected, found_failed
1840 def create_intersite_connections(self):
1841 """Computes an NC replica graph for each NC replica that "should be
1842 present" on the local DC or "is present" on any DC in the same site
1843 as the local DC. For each edge directed to an NC replica on such a
1844 DC from an NC replica on a DC in another site, the KCC creates an
1845 nTDSConnection object to imply that edge if one does not already
1848 Modifies self.kept_connections - A set of nTDSConnection
1849 objects for edges that are directed
1850 to the local DC's site in one or more NC replica graphs.
1852 returns: True if spanning trees were created for all NC replica
1853 graphs, otherwise False.
1855 all_connected = True
1856 self.kept_connections = set()
1858 # LET crossRefList be the set containing each object o of class
1859 # crossRef such that o is a child of the CN=Partitions child of the
1862 # FOR each crossRef object cr in crossRefList
1863 # IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1864 # is clear in cr!systemFlags, skip cr.
1865 # LET g be the GRAPH return of SetupGraph()
1867 for part in self.part_table.values():
1869 if not part.is_enabled():
1872 if part.is_foreign():
1875 graph = self.setup_graph(part)
1877 # Create nTDSConnection objects, routing replication traffic
1878 # around "failed" DCs.
1879 found_failed = False
1881 connected, found_failed = self.create_connections(graph,
1884 DEBUG("with detect_failed: connected %s Found failed %s" %
1885 (connected, found_failed))
1887 all_connected = False
1890 # One or more failed DCs preclude use of the ideal NC
1891 # replica graph. Add connections for the ideal graph.
1892 self.create_connections(graph, part, False)
1894 return all_connected
1896 def intersite(self):
1897 """The head method for generating the inter-site KCC replica
1898 connection graph and attendant nTDSConnection objects
1901 Produces self.kept_connections set of NTDS Connections
1902 that should be kept during subsequent pruning process.
1904 ::return (True or False): (True) if the produced NC replica
1905 graph connects all sites that need to be connected
1910 mysite = self.my_site
1911 all_connected = True
1913 DEBUG_FN("intersite(): enter")
1915 # Determine who is the ISTG
1917 mysite.select_istg(self.samdb, mydsa, ro=True)
1919 mysite.select_istg(self.samdb, mydsa, ro=False)
1921 # Test whether local site has topology disabled
1922 if mysite.is_intersite_topology_disabled():
1923 DEBUG_FN("intersite(): exit disabled all_connected=%d" %
1925 return all_connected
1927 if not mydsa.is_istg():
1928 DEBUG_FN("intersite(): exit not istg all_connected=%d" %
1930 return all_connected
1932 self.merge_failed_links()
1934 # For each NC with an NC replica that "should be present" on the
1935 # local DC or "is present" on any DC in the same site as the
1936 # local DC, the KCC constructs a site graph--a precursor to an NC
1937 # replica graph. The site connectivity for a site graph is defined
1938 # by objects of class interSiteTransport, siteLink, and
1939 # siteLinkBridge in the config NC.
1941 all_connected = self.create_intersite_connections()
1943 DEBUG_FN("intersite(): exit all_connected=%d" % all_connected)
1944 return all_connected
1946 def update_rodc_connection(self):
1947 """Runs when the local DC is an RODC and updates the RODC NTFRS
1950 # Given an nTDSConnection object cn1, such that cn1.options contains
1951 # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1952 # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1953 # that the following is true:
1955 # cn1.fromServer = cn2.fromServer
1956 # cn1.schedule = cn2.schedule
1958 # If no such cn2 can be found, cn1 is not modified.
1959 # If no such cn1 can be found, nothing is modified by this task.
1961 if not self.my_dsa.is_ro():
1964 all_connections = self.my_dsa.connect_table.values()
1965 ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1966 rw_connections = [x for x in all_connections
1967 if x not in ro_connections]
1969 # XXX here we are dealing with multiple RODC_TOPO connections,
1970 # if they exist. It is not clear whether the spec means that
1971 # or if it ever arises.
1972 if rw_connections and ro_connections:
1973 for con in ro_connections:
1974 cn2 = rw_connections[0]
1975 con.from_dnstr = cn2.from_dnstr
1976 con.schedule = cn2.schedule
1977 con.to_be_modified = True
1979 self.my_dsa.commit_connections(self.samdb, ro=self.readonly)
1981 def intrasite_max_node_edges(self, node_count):
1982 """Returns the maximum number of edges directed to a node in
1983 the intrasite replica graph.
1985 The KCC does not create more
1986 than 50 edges directed to a single DC. To optimize replication,
1987 we compute that each node should have n+2 total edges directed
1988 to it such that (n) is the smallest non-negative integer
1989 satisfying (node_count <= 2*(n*n) + 6*n + 7)
1991 (If the number of edges is m (i.e. n + 2), that is the same as
1992 2 * m*m - 2 * m + 3).
2002 :param node_count: total number of nodes in the replica graph
2004 The intention is that there should be no more than 3 hops
2005 between any two DSAs at a site. With up to 7 nodes the 2 edges
2006 of the ring are enough; any configuration of extra edges with
2007 8 nodes will be enough. It is less clear that the 3 hop
2008 guarantee holds at e.g. 15 nodes in degenerate cases, but
2009 those are quite unlikely given the extra edges are randomly
2014 if node_count <= (2 * (n * n) + (6 * n) + 7):
2022 def construct_intrasite_graph(self, site_local, dc_local,
2023 nc_x, gc_only, detect_stale):
2025 # We're using the MS notation names here to allow
2026 # correlation back to the published algorithm.
2028 # nc_x - naming context (x) that we are testing if it
2029 # "should be present" on the local DC
2030 # f_of_x - replica (f) found on a DC (s) for NC (x)
2031 # dc_s - DC where f_of_x replica was found
2032 # dc_local - local DC that potentially needs a replica
2034 # r_list - replica list R
2035 # p_of_x - replica (p) is partial and found on a DC (s)
2037 # l_of_x - replica (l) is the local replica for NC (x)
2038 # that should appear on the local DC
2039 # r_len = is length of replica list |R|
2041 # If the DSA doesn't need a replica for this
2042 # partition (NC x) then continue
2043 needed, ro, partial = nc_x.should_be_present(dc_local)
2045 debug.DEBUG_YELLOW("construct_intrasite_graph(): enter" +
2046 "\n\tgc_only=%d" % gc_only +
2047 "\n\tdetect_stale=%d" % detect_stale +
2048 "\n\tneeded=%s" % needed +
2050 "\n\tpartial=%s" % partial +
2054 debug.DEBUG_RED("%s lacks 'should be present' status, "
2055 "aborting construct_intersite_graph!" %
2059 # Create a NCReplica that matches what the local replica
2060 # should say. We'll use this below in our r_list
2061 l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid,
2064 l_of_x.identify_by_basedn(self.samdb)
2066 l_of_x.rep_partial = partial
2069 # Add this replica that "should be present" to the
2070 # needed replica table for this DSA
2071 dc_local.add_needed_replica(l_of_x)
2075 # Let R be a sequence containing each writable replica f of x
2076 # such that f "is present" on a DC s satisfying the following
2079 # * s is a writable DC other than the local DC.
2081 # * s is in the same site as the local DC.
2083 # * If x is a read-only full replica and x is a domain NC,
2084 # then the DC's functional level is at least
2085 # DS_BEHAVIOR_WIN2008.
2087 # * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
2088 # in the options attribute of the site settings object for
2089 # the local DC's site, or no tuple z exists in the
2090 # kCCFailedLinks or kCCFailedConnections variables such
2091 # that z.UUIDDsa is the objectGUID of the nTDSDSA object
2092 # for s, z.FailureCount > 0, and the current time -
2093 # z.TimeFirstFailure > 2 hours.
2097 # We'll loop thru all the DSAs looking for
2098 # writeable NC replicas that match the naming
2099 # context dn for (nc_x)
2101 for dc_s in self.my_site.dsa_table.values():
2102 # If this partition (nc_x) doesn't appear as a
2103 # replica (f_of_x) on (dc_s) then continue
2104 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2107 # Pull out the NCReplica (f) of (x) with the dn
2108 # that matches NC (x) we are examining.
2109 f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2111 # Replica (f) of NC (x) must be writable
2115 # Replica (f) of NC (x) must satisfy the
2116 # "is present" criteria for DC (s) that
2118 if not f_of_x.is_present():
2121 # DC (s) must be a writable DSA other than
2122 # my local DC. In other words we'd only replicate
2123 # from other writable DC
2124 if dc_s.is_ro() or dc_s is dc_local:
2127 # Certain replica graphs are produced only
2128 # for global catalogs, so test against
2129 # method input parameter
2130 if gc_only and not dc_s.is_gc():
2133 # DC (s) must be in the same site as the local DC
2134 # as this is the intra-site algorithm. This is
2135 # handled by virtue of placing DSAs in per
2136 # site objects (see enclosing for() loop)
2138 # If NC (x) is intended to be read-only full replica
2139 # for a domain NC on the target DC then the source
2140 # DC should have functional level at minimum WIN2008
2142 # Effectively we're saying that in order to replicate
2143 # to a targeted RODC (which was introduced in Windows 2008)
2144 # then we have to replicate from a DC that is also minimally
2147 # You can also see this requirement in the MS special
2148 # considerations for RODC which state that to deploy
2149 # an RODC, at least one writable domain controller in
2150 # the domain must be running Windows Server 2008
2151 if ro and not partial and nc_x.nc_type == NCType.domain:
2152 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2155 # If we haven't been told to turn off stale connection
2156 # detection and this dsa has a stale connection then
2158 if detect_stale and self.is_stale_link_connection(dc_s):
2161 # Replica meets criteria. Add it to table indexed
2162 # by the GUID of the DC that it appears on
2163 r_list.append(f_of_x)
2165 # If a partial (not full) replica of NC (x) "should be present"
2166 # on the local DC, append to R each partial replica (p of x)
2167 # such that p "is present" on a DC satisfying the same
2168 # criteria defined above for full replica DCs.
2170 # XXX This loop and the previous one differ only in whether
2171 # the replica is partial or not. here we only accept partial
2172 # (because we're partial); before we only accepted full. Order
2173 # doen't matter (the list is sorted a few lines down) so these
2174 # loops could easily be merged. Or this could be a helper
2178 # Now we loop thru all the DSAs looking for
2179 # partial NC replicas that match the naming
2180 # context dn for (NC x)
2181 for dc_s in self.my_site.dsa_table.values():
2183 # If this partition NC (x) doesn't appear as a
2184 # replica (p) of NC (x) on the dsa DC (s) then
2186 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2189 # Pull out the NCReplica with the dn that
2190 # matches NC (x) we are examining.
2191 p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2193 # Replica (p) of NC (x) must be partial
2194 if not p_of_x.is_partial():
2197 # Replica (p) of NC (x) must satisfy the
2198 # "is present" criteria for DC (s) that
2200 if not p_of_x.is_present():
2203 # DC (s) must be a writable DSA other than
2204 # my DSA. In other words we'd only replicate
2205 # from other writable DSA
2206 if dc_s.is_ro() or dc_s is dc_local:
2209 # Certain replica graphs are produced only
2210 # for global catalogs, so test against
2211 # method input parameter
2212 if gc_only and not dc_s.is_gc():
2215 # If we haven't been told to turn off stale connection
2216 # detection and this dsa has a stale connection then
2218 if detect_stale and self.is_stale_link_connection(dc_s):
2221 # Replica meets criteria. Add it to table indexed
2222 # by the GUID of the DSA that it appears on
2223 r_list.append(p_of_x)
2225 # Append to R the NC replica that "should be present"
2227 r_list.append(l_of_x)
2229 r_list.sort(sort_replica_by_dsa_guid)
2232 max_node_edges = self.intrasite_max_node_edges(r_len)
2234 # Add a node for each r_list element to the replica graph
2237 node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2238 graph_list.append(node)
2240 # For each r(i) from (0 <= i < |R|-1)
2242 while i < (r_len-1):
2243 # Add an edge from r(i) to r(i+1) if r(i) is a full
2244 # replica or r(i+1) is a partial replica
2245 if not r_list[i].is_partial() or r_list[i+1].is_partial():
2246 graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
2248 # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2249 # replica or ri is a partial replica.
2250 if not r_list[i+1].is_partial() or r_list[i].is_partial():
2251 graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
2254 # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2255 # or r0 is a partial replica.
2256 if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
2257 graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
2259 # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2260 # r|R|-1 is a partial replica.
2261 if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
2262 graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
2264 DEBUG("r_list is length %s" % len(r_list))
2265 DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr))
2268 do_dot_files = self.dot_files and self.debug
2269 if self.verify or do_dot_files:
2271 dot_vertices = set()
2272 for v1 in graph_list:
2273 dot_vertices.add(v1.dsa_dnstr)
2274 for v2 in v1.edge_from:
2275 dot_edges.append((v2, v1.dsa_dnstr))
2276 dot_vertices.add(v2)
2278 verify_properties = ('connected', 'directed_double_ring_or_small')
2279 verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2280 label='%s__%s__%s' % (site_local.site_dnstr,
2281 nctype_lut[nc_x.nc_type],
2283 properties=verify_properties, debug=DEBUG,
2285 dot_files=do_dot_files, directed=True)
2287 # For each existing nTDSConnection object implying an edge
2288 # from rj of R to ri such that j != i, an edge from rj to ri
2289 # is not already in the graph, and the total edges directed
2290 # to ri is less than n+2, the KCC adds that edge to the graph.
2291 for vertex in graph_list:
2292 dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2293 for connect in dsa.connect_table.values():
2294 remote = connect.from_dnstr
2295 if remote in self.my_site.dsa_table:
2296 vertex.add_edge_from(remote)
2298 DEBUG('reps are: %s' % ' '.join(x.rep_dsa_dnstr for x in r_list))
2299 DEBUG('dsas are: %s' % ' '.join(x.dsa_dnstr for x in graph_list))
2301 for tnode in graph_list:
2302 # To optimize replication latency in sites with many NC
2303 # replicas, the KCC adds new edges directed to ri to bring
2304 # the total edges to n+2, where the NC replica rk of R
2305 # from which the edge is directed is chosen at random such
2306 # that k != i and an edge from rk to ri is not already in
2309 # Note that the KCC tech ref does not give a number for
2310 # the definition of "sites with many NC replicas". At a
2311 # bare minimum to satisfy n+2 edges directed at a node we
2312 # have to have at least three replicas in |R| (i.e. if n
2313 # is zero then at least replicas from two other graph
2314 # nodes may direct edges to us).
2315 if r_len >= 3 and not tnode.has_sufficient_edges():
2316 candidates = [x for x in graph_list if
2318 x.dsa_dnstr not in tnode.edge_from)]
2320 debug.DEBUG_BLUE("looking for random link for %s. r_len %d, "
2321 "graph len %d candidates %d"
2322 % (tnode.dsa_dnstr, r_len, len(graph_list),
2325 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2327 while candidates and not tnode.has_sufficient_edges():
2328 other = random.choice(candidates)
2329 DEBUG("trying to add candidate %s" % other.dsa_dstr)
2330 if not tnode.add_edge_from(other):
2331 debug.DEBUG_RED("could not add %s" % other.dsa_dstr)
2332 candidates.remove(other)
2334 DEBUG_FN("not adding links to %s: nodes %s, links is %s/%s" %
2335 (tnode.dsa_dnstr, r_len, len(tnode.edge_from),
2338 # Print the graph node in debug mode
2339 DEBUG_FN("%s" % tnode)
2341 # For each edge directed to the local DC, ensure a nTDSConnection
2342 # points to us that satisfies the KCC criteria
2344 if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2345 tnode.add_connections_from_edges(dc_local)
2347 if self.verify or do_dot_files:
2349 dot_vertices = set()
2350 for v1 in graph_list:
2351 dot_vertices.add(v1.dsa_dnstr)
2352 for v2 in v1.edge_from:
2353 dot_edges.append((v2, v1.dsa_dnstr))
2354 dot_vertices.add(v2)
2356 verify_properties = ('connected', 'directed_double_ring_or_small')
2357 verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2358 label='%s__%s__%s' % (site_local.site_dnstr,
2359 nctype_lut[nc_x.nc_type],
2361 properties=verify_properties, debug=DEBUG,
2363 dot_files=do_dot_files, directed=True)
2365 def intrasite(self):
2366 """The head method for generating the intra-site KCC replica
2367 connection graph and attendant nTDSConnection objects
2373 DEBUG_FN("intrasite(): enter")
2375 # Test whether local site has topology disabled
2376 mysite = self.my_site
2377 if mysite.is_intrasite_topology_disabled():
2380 detect_stale = (not mysite.is_detect_stale_disabled())
2381 for connect in mydsa.connect_table.values():
2382 if connect.to_be_added:
2383 debug.DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2385 # Loop thru all the partitions, with gc_only False
2386 for partdn, part in self.part_table.items():
2387 self.construct_intrasite_graph(mysite, mydsa, part, False,
2389 for connect in mydsa.connect_table.values():
2390 if connect.to_be_added:
2391 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2393 # If the DC is a GC server, the KCC constructs an additional NC
2394 # replica graph (and creates nTDSConnection objects) for the
2395 # config NC as above, except that only NC replicas that "are present"
2396 # on GC servers are added to R.
2397 for connect in mydsa.connect_table.values():
2398 if connect.to_be_added:
2399 debug.DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2401 # Do it again, with gc_only True
2402 for partdn, part in self.part_table.items():
2403 if part.is_config():
2404 self.construct_intrasite_graph(mysite, mydsa, part, True,
2407 # The DC repeats the NC replica graph computation and nTDSConnection
2408 # creation for each of the NC replica graphs, this time assuming
2409 # that no DC has failed. It does so by re-executing the steps as
2410 # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2411 # set in the options attribute of the site settings object for
2412 # the local DC's site. (ie. we set "detec_stale" flag to False)
2413 for connect in mydsa.connect_table.values():
2414 if connect.to_be_added:
2415 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2417 # Loop thru all the partitions.
2418 for partdn, part in self.part_table.items():
2419 self.construct_intrasite_graph(mysite, mydsa, part, False,
2420 False) # don't detect stale
2422 # If the DC is a GC server, the KCC constructs an additional NC
2423 # replica graph (and creates nTDSConnection objects) for the
2424 # config NC as above, except that only NC replicas that "are present"
2425 # on GC servers are added to R.
2426 for connect in mydsa.connect_table.values():
2427 if connect.to_be_added:
2428 debug.DEBUG_RED("TO BE ADDED:\n%s" % connect)
2430 for partdn, part in self.part_table.items():
2431 if part.is_config():
2432 self.construct_intrasite_graph(mysite, mydsa, part, True,
2433 False) # don't detect stale
2436 # Display any to be added or modified repsFrom
2437 for connect in mydsa.connect_table.values():
2438 if connect.to_be_deleted:
2439 logger.info("TO BE DELETED:\n%s" % connect)
2440 if connect.to_be_modified:
2441 logger.info("TO BE MODIFIED:\n%s" % connect)
2442 if connect.to_be_added:
2443 debug.DEBUG_GREEN("TO BE ADDED:\n%s" % connect)
2445 mydsa.commit_connections(self.samdb, ro=True)
2447 # Commit any newly created connections to the samdb
2448 mydsa.commit_connections(self.samdb)
2450 def list_dsas(self):
2451 """Compile a comprehensive list of DSA DNs
2453 These are all the DSAs on all the sites that KCC would be
2456 This method is not idempotent and may not work correctly in
2457 sequence with KCC.run().
2459 :return: a list of DSA DN strings.
2464 self.load_all_sites()
2465 self.load_all_partitions()
2466 self.load_all_transports()
2467 self.load_all_sitelinks()
2469 for site in self.site_table.values():
2470 dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2471 for dsa in site.dsa_table.values()])
2474 def load_samdb(self, dburl, lp, creds):
2475 """Load the database using an url, loadparm, and credentials
2477 :param dburl: a database url.
2478 :param lp: a loadparm object.
2479 :param cred: a Credentials object.
2481 self.samdb = SamDB(url=dburl,
2482 session_info=system_session(),
2483 credentials=creds, lp=lp)
2485 def plot_all_connections(self, basename, verify_properties=()):
2486 verify = verify_properties and self.verify
2487 plot = self.dot_files
2488 if not (verify or plot):
2496 for dsa in self.dsa_by_dnstr.values():
2497 dot_vertices.append(dsa.dsa_dnstr)
2499 vertex_colours.append('#cc0000')
2501 vertex_colours.append('#0000cc')
2502 for con in dsa.connect_table.values():
2503 if con.is_rodc_topology():
2504 edge_colours.append('red')
2506 edge_colours.append('blue')
2507 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2509 verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2510 label=self.my_dsa_dnstr, properties=verify_properties,
2511 debug=DEBUG, verify=verify, dot_files=plot,
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 """Method to perform a complete run of the KCC and
2518 produce an updated topology for subsequent NC replica
2519 syncronization between domain controllers
2521 # We may already have a samdb setup if we are
2522 # currently importing an ldif for a test run
2523 if self.samdb is None:
2525 self.load_samdb(dburl, lp, creds)
2526 except ldb.LdbError, (num, msg):
2527 logger.error("Unable to open sam database %s : %s" %
2531 if forced_local_dsa:
2532 self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" %
2540 self.load_all_sites()
2541 self.load_all_partitions()
2542 self.load_all_transports()
2543 self.load_all_sitelinks()
2545 if self.verify or self.dot_files:
2547 for site in self.site_table.values():
2548 guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2550 in site.dsa_table.items())
2552 self.plot_all_connections('dsa_initial')
2555 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2556 for dnstr, c_rep in current_reps.items():
2557 DEBUG("c_rep %s" % c_rep)
2558 dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2560 verify_and_dot('dsa_repsFrom_initial', dot_edges,
2561 directed=True, label=self.my_dsa_dnstr,
2562 properties=(), debug=DEBUG, verify=self.verify,
2563 dot_files=self.dot_files)
2566 for site in self.site_table.values():
2567 for dsa in site.dsa_table.values():
2568 current_reps, needed_reps = dsa.get_rep_tables()
2569 for dn_str, rep in current_reps.items():
2570 for reps_from in rep.rep_repsFrom:
2571 DEBUG("rep %s" % rep)
2572 dsa_guid = str(reps_from.source_dsa_obj_guid)
2573 dsa_dn = guid_to_dnstr[dsa_guid]
2574 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2576 verify_and_dot('dsa_repsFrom_initial_all', dot_edges,
2577 directed=True, label=self.my_dsa_dnstr,
2578 properties=(), debug=DEBUG, verify=self.verify,
2579 dot_files=self.dot_files)
2582 for link in self.sitelink_table.values():
2583 for a, b in itertools.combinations(link.site_list, 2):
2584 dot_edges.append((str(a), str(b)))
2585 properties = ('connected',)
2586 verify_and_dot('dsa_sitelink_initial', dot_edges,
2588 label=self.my_dsa_dnstr, properties=properties,
2589 debug=DEBUG, verify=self.verify,
2590 dot_files=self.dot_files)
2592 if forget_local_links:
2593 for dsa in self.my_site.dsa_table.values():
2594 dsa.connect_table = {k: v for k, v in
2595 dsa.connect_table.items()
2596 if v.is_rodc_topology()}
2597 self.plot_all_connections('dsa_forgotten_local')
2599 if forget_intersite_links:
2600 for site in self.site_table.values():
2601 for dsa in site.dsa_table.values():
2602 dsa.connect_table = {k: v for k, v in
2603 dsa.connect_table.items()
2604 if site is self.my_site and
2605 v.is_rodc_topology()}
2607 self.plot_all_connections('dsa_forgotten_all')
2608 # These are the published steps (in order) for the
2609 # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2612 self.refresh_failed_links_connections()
2618 all_connected = self.intersite()
2621 self.remove_unneeded_ntdsconn(all_connected)
2624 self.translate_ntdsconn()
2627 self.remove_unneeded_failed_links_connections()
2630 self.update_rodc_connection()
2632 if self.verify or self.dot_files:
2633 self.plot_all_connections('dsa_final',
2634 ('connected', 'forest_of_rings'))
2636 debug.DEBUG_MAGENTA("there are %d dsa guids" %
2641 my_dnstr = self.my_dsa.dsa_dnstr
2642 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2643 for dnstr, n_rep in needed_reps.items():
2644 for reps_from in n_rep.rep_repsFrom:
2645 guid_str = str(reps_from.source_dsa_obj_guid)
2646 dot_edges.append((my_dnstr, guid_to_dnstr[guid_str]))
2647 edge_colors.append('#' + str(n_rep.nc_guid)[:6])
2649 verify_and_dot('dsa_repsFrom_final', dot_edges, directed=True,
2650 label=self.my_dsa_dnstr,
2651 properties=(), debug=DEBUG, verify=self.verify,
2652 dot_files=self.dot_files,
2653 edge_colors=edge_colors)
2657 for site in self.site_table.values():
2658 for dsa in site.dsa_table.values():
2659 current_reps, needed_reps = dsa.get_rep_tables()
2660 for n_rep in needed_reps.values():
2661 for reps_from in n_rep.rep_repsFrom:
2662 dsa_guid = str(reps_from.source_dsa_obj_guid)
2663 dsa_dn = guid_to_dnstr[dsa_guid]
2664 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2666 verify_and_dot('dsa_repsFrom_final_all', dot_edges,
2667 directed=True, label=self.my_dsa_dnstr,
2668 properties=(), debug=DEBUG, verify=self.verify,
2669 dot_files=self.dot_files)
2676 def import_ldif(self, dburl, lp, creds, ldif_file):
2677 """Import all objects and attributes that are relevent
2678 to the KCC algorithms from a previously exported LDIF file.
2680 The point of this function is to allow a programmer/debugger to
2681 import an LDIF file with non-security relevent information that
2682 was previously extracted from a DC database. The LDIF file is used
2683 to create a temporary abbreviated database. The KCC algorithm can
2684 then run against this abbreviated database for debug or test
2685 verification that the topology generated is computationally the
2686 same between different OSes and algorithms.
2688 :param dburl: path to the temporary abbreviated db to create
2689 :param ldif_file: path to the ldif file to import
2692 self.samdb = ldif_utils.ldif_to_samdb(dburl, lp, ldif_file,
2693 self.forced_local_dsa)
2694 except ldif_utils.LdifError, e:
2699 def export_ldif(self, dburl, lp, creds, ldif_file):
2700 """Routine to extract all objects and attributes that are relevent
2701 to the KCC algorithms from a DC database.
2703 The point of this function is to allow a programmer/debugger to
2704 extract an LDIF file with non-security relevent information from
2705 a DC database. The LDIF file can then be used to "import" via
2706 the import_ldif() function this file into a temporary abbreviated
2707 database. The KCC algorithm can then run against this abbreviated
2708 database for debug or test verification that the topology generated
2709 is computationally the same between different OSes and algorithms.
2711 :param dburl: LDAP database URL to extract info from
2712 :param ldif_file: output LDIF file name to create
2715 ldif_utils.samdb_to_ldif_file(self.samdb, dburl, lp, creds,
2717 except ldif_utils.LdifError, e: