3 # Compute our KCC topology
5 # Copyright (C) Dave Craft 2011
6 # Copyright (C) Andrew Bartlett 2015
8 # Andrew Bartlett's alleged work performed by his underlings Douglas
9 # Bagnall and Garming Sam.
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 3 of the License, or
14 # (at your option) any later version.
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with this program. If not, see <http://www.gnu.org/licenses/>.
29 # ensure we get messages out immediately, so they get in the samba logs,
30 # and don't get swallowed by a timeout
31 os.environ['PYTHONUNBUFFERED'] = '1'
33 # forcing GMT avoids a problem in some timezones with kerberos. Both MIT
34 # heimdal can get mutual authentication errors due to the 24 second difference
35 # between UTC and GMT when using some zone files (eg. the PDT zone from
37 os.environ["TZ"] = "GMT"
39 # Find right directory when running from source tree
40 sys.path.insert(0, "bin/python")
47 from functools import partial
55 from samba.auth import system_session
56 from samba.samdb import SamDB
57 from samba.dcerpc import drsuapi
58 from samba.kcc_utils import *
59 from samba.graph_utils import *
60 from samba import ldif_utils
64 """The Knowledge Consistency Checker class.
66 A container for objects and methods allowing a run of the KCC. Produces a
67 set of connections in the samdb for which the Distributed Replication
68 Service can then utilize to replicate naming contexts
70 :param unix_now: The putative current time in seconds since 1970.
71 :param read_only: Don't write to the database.
72 :param verify: Check topological invariants for the generated graphs
73 :param debug: Write verbosely to stderr.
74 "param dot_files: write Graphviz files in /tmp showing topology
77 """Initializes the partitions class which can hold
78 our local DCs partitions or all the partitions in
81 self.part_table = {} # partition objects
83 self.transport_table = {}
84 self.ip_transport = None
85 self.sitelink_table = {}
86 self.dsa_by_dnstr = {}
89 self.get_dsa_by_guidstr = self.dsa_by_guid.get
90 self.get_dsa = self.dsa_by_dnstr.get
92 # TODO: These should be backed by a 'permanent' store so that when
93 # calling DRSGetReplInfo with DS_REPL_INFO_KCC_DSA_CONNECT_FAILURES,
94 # the failure information can be returned
95 self.kcc_failed_links = {}
96 self.kcc_failed_connections = set()
98 # Used in inter-site topology computation. A list
99 # of connections (by NTDSConnection object) that are
100 # to be kept when pruning un-needed NTDS Connections
101 self.kept_connections = set()
103 self.my_dsa_dnstr = None # My dsa DN
104 self.my_dsa = None # My dsa object
106 self.my_site_dnstr = None
111 def load_all_transports(self):
112 """Loads the inter-site transport objects for Sites
115 :raise KCCError: if no IP transport is found
118 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
119 self.samdb.get_config_basedn(),
120 scope=ldb.SCOPE_SUBTREE,
121 expression="(objectClass=interSiteTransport)")
122 except ldb.LdbError, (enum, estr):
123 raise KCCError("Unable to find inter-site transports - (%s)" %
129 transport = Transport(dnstr)
131 transport.load_transport(self.samdb)
132 self.transport_table.setdefault(str(transport.guid),
134 if transport.name == 'IP':
135 self.ip_transport = transport
137 if self.ip_transport is None:
138 raise KCCError("there doesn't seem to be an IP transport")
140 def load_all_sitelinks(self):
141 """Loads the inter-site siteLink objects
144 :raise KCCError: if site-links aren't found
147 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
148 self.samdb.get_config_basedn(),
149 scope=ldb.SCOPE_SUBTREE,
150 expression="(objectClass=siteLink)")
151 except ldb.LdbError, (enum, estr):
152 raise KCCError("Unable to find inter-site siteLinks - (%s)" % estr)
158 if dnstr in self.sitelink_table:
161 sitelink = SiteLink(dnstr)
163 sitelink.load_sitelink(self.samdb)
165 # Assign this siteLink to table
167 self.sitelink_table[dnstr] = sitelink
169 def load_site(self, dn_str):
170 """Helper for load_my_site and load_all_sites.
172 Put all the site's DSAs into the KCC indices.
174 :param dn_str: a site dn_str
175 :return: the Site object pertaining to the dn_str
177 site = Site(dn_str, unix_now)
178 site.load_site(self.samdb)
180 # We avoid replacing the site with an identical copy in case
181 # somewhere else has a reference to the old one, which would
182 # lead to all manner of confusion and chaos.
183 guid = str(site.site_guid)
184 if guid not in self.site_table:
185 self.site_table[guid] = site
186 self.dsa_by_dnstr.update(site.dsa_table)
187 self.dsa_by_guid.update((str(x.dsa_guid), x)
188 for x in site.dsa_table.values())
190 return self.site_table[guid]
192 def load_my_site(self):
193 """Load the Site object for the local DSA.
197 self.my_site_dnstr = ("CN=%s,CN=Sites,%s" % (
198 self.samdb.server_site_name(),
199 self.samdb.get_config_basedn()))
201 self.my_site = self.load_site(self.my_site_dnstr)
203 def load_all_sites(self):
204 """Discover all sites and create Site objects.
207 :raise: KCCError if sites can't be found
210 res = self.samdb.search("CN=Sites,%s" %
211 self.samdb.get_config_basedn(),
212 scope=ldb.SCOPE_SUBTREE,
213 expression="(objectClass=site)")
214 except ldb.LdbError, (enum, estr):
215 raise KCCError("Unable to find sites - (%s)" % estr)
218 sitestr = str(msg.dn)
219 self.load_site(sitestr)
221 def load_my_dsa(self):
222 """Discover my nTDSDSA dn thru the rootDSE entry
225 :raise: KCCError if DSA can't be found
227 dn = ldb.Dn(self.samdb, "<GUID=%s>" % self.samdb.get_ntds_GUID())
229 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
230 attrs=["objectGUID"])
231 except ldb.LdbError, (enum, estr):
232 logger.warning("Search for %s failed: %s. This typically happens"
233 " in --importldif mode due to lack of module"
234 " support.", dn, estr)
236 # We work around the failure above by looking at the
237 # dsServiceName that was put in the fake rootdse by
238 # the --exportldif, rather than the
239 # samdb.get_ntds_GUID(). The disadvantage is that this
240 # mode requires we modify the @ROOTDSE dnq to support
242 service_name_res = self.samdb.search(base="",
243 scope=ldb.SCOPE_BASE,
244 attrs=["dsServiceName"])
245 dn = ldb.Dn(self.samdb,
246 service_name_res[0]["dsServiceName"][0])
248 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
249 attrs=["objectGUID"])
250 except ldb.LdbError, (enum, estr):
251 raise KCCError("Unable to find my nTDSDSA - (%s)" % estr)
254 raise KCCError("Unable to find my nTDSDSA at %s" %
257 ntds_guid = misc.GUID(self.samdb.get_ntds_GUID())
258 if misc.GUID(res[0]["objectGUID"][0]) != ntds_guid:
259 raise KCCError("Did not find the GUID we expected,"
260 " perhaps due to --importldif")
262 self.my_dsa_dnstr = str(res[0].dn)
264 self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
266 if self.my_dsa_dnstr not in self.dsa_by_dnstr:
267 DEBUG_DARK_YELLOW("my_dsa %s isn't in self.dsas_by_dnstr:"
268 " it must be RODC.\n"
269 "Let's add it, because my_dsa is special!\n"
270 "(likewise for self.dsa_by_guid of course)" %
273 self.dsa_by_dnstr[self.my_dsa_dnstr] = self.my_dsa
274 self.dsa_by_guid[str(self.my_dsa.dsa_guid)] = self.my_dsa
276 def load_all_partitions(self):
277 """Discover and load all partitions.
279 Each NC is inserted into the part_table by partition
280 dn string (not the nCName dn string)
283 :raise: KCCError if partitions can't be found
286 res = self.samdb.search("CN=Partitions,%s" %
287 self.samdb.get_config_basedn(),
288 scope=ldb.SCOPE_SUBTREE,
289 expression="(objectClass=crossRef)")
290 except ldb.LdbError, (enum, estr):
291 raise KCCError("Unable to find partitions - (%s)" % estr)
294 partstr = str(msg.dn)
297 if partstr in self.part_table:
300 part = Partition(partstr)
302 part.load_partition(self.samdb)
303 self.part_table[partstr] = part
305 def should_be_present_test(self):
306 """Enumerate all loaded partitions and DSAs in local
307 site and test if NC should be present as replica
309 for partdn, part in self.part_table.items():
310 for dsadn, dsa in self.my_site.dsa_table.items():
311 needed, ro, partial = part.should_be_present(dsa)
312 logger.info("dsadn:%s\nncdn:%s\nneeded=%s:ro=%s:partial=%s\n" %
313 (dsadn, part.nc_dnstr, needed, ro, partial))
315 def refresh_failed_links_connections(self):
316 """Based on MS-ADTS 6.2.2.1"""
318 # Instead of NULL link with failure_count = 0, the tuple is
321 # LINKS: Refresh failed links
322 self.kcc_failed_links = {}
323 current, needed = self.my_dsa.get_rep_tables()
324 for replica in current.values():
325 # For every possible connection to replicate
326 for reps_from in replica.rep_repsFrom:
327 failure_count = reps_from.consecutive_sync_failures
328 if failure_count <= 0:
331 dsa_guid = str(reps_from.source_dsa_obj_guid)
332 time_first_failure = reps_from.last_success
333 last_result = reps_from.last_attempt
334 dns_name = reps_from.dns_name1
336 f = self.kcc_failed_links.get(dsa_guid)
338 f = KCCFailedObject(dsa_guid, failure_count,
339 time_first_failure, last_result,
341 self.kcc_failed_links[dsa_guid] = f
342 #elif f.failure_count == 0:
343 # f.failure_count = failure_count
344 # f.time_first_failure = time_first_failure
345 # f.last_result = last_result
347 f.failure_count = max(f.failure_count, failure_count)
348 f.time_first_failure = min(f.time_first_failure,
350 f.last_result = last_result
352 # CONNECTIONS: Refresh failed connections
353 restore_connections = set()
354 if opts.attempt_live_connections:
355 DEBUG("refresh_failed_links: checking if links are still down")
356 for connection in self.kcc_failed_connections:
358 drs_utils.drsuapi_connect(connection.dns_name, lp, creds)
359 # Failed connection is no longer failing
360 restore_connections.add(connection)
361 except drs_utils.drsException:
362 # Failed connection still failing
363 connection.failure_count += 1
365 DEBUG("refresh_failed_links: not checking live links because we\n"
366 "weren't asked to --attempt-live-connections")
368 # Remove the restored connections from the failed connections
369 self.kcc_failed_connections.difference_update(restore_connections)
371 def is_stale_link_connection(self, target_dsa):
372 """Check whether a link to a remote DSA is stale
374 Used in MS-ADTS 6.2.2.2 Intrasite Connection Creation
376 Returns True if the remote seems to have been down for at
377 least two hours, otherwise False.
379 :param target_dsa: the remote DSA object
380 :return: True if link is stale, otherwise False
382 failed_link = self.kcc_failed_links.get(str(target_dsa.dsa_guid))
384 # failure_count should be > 0, but check anyways
385 if failed_link.failure_count > 0:
386 unix_first_failure = \
387 nttime2unix(failed_link.time_first_failure)
388 # TODO guard against future
389 if unix_first_failure > unix_now:
390 logger.error("The last success time attribute for \
391 repsFrom is in the future!")
393 # Perform calculation in seconds
394 if (unix_now - unix_first_failure) > 60 * 60 * 2:
401 # TODO: This should be backed by some form of local database
402 def remove_unneeded_failed_links_connections(self):
403 # Remove all tuples in kcc_failed_links where failure count = 0
404 # In this implementation, this should never happen.
406 # Remove all connections which were not used this run or connections
407 # that became active during this run.
410 def remove_unneeded_ntdsconn(self, all_connected):
411 """Remove unneeded NTDS Connections once topology is calculated
413 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections
415 :param all_connected: indicates whether all sites are connected
420 # New connections won't have GUIDs which are needed for
422 for cn_conn in mydsa.connect_table.values():
423 if cn_conn.guid is None:
425 cn_conn.guid = misc.GUID(str(uuid.uuid4()))
426 cn_conn.whenCreated = nt_now
428 cn_conn.load_connection(self.samdb)
430 for cn_conn in mydsa.connect_table.values():
432 s_dnstr = cn_conn.get_from_dnstr()
434 cn_conn.to_be_deleted = True
437 # Get the source DSA no matter what site
438 # XXX s_dsa is NEVER USED. It will be removed.
439 s_dsa = self.get_dsa(s_dnstr)
441 #XXX should an RODC be regarded as same site
442 same_site = s_dnstr in self.my_site.dsa_table
444 # Given an nTDSConnection object cn, if the DC with the
445 # nTDSDSA object dc that is the parent object of cn and
446 # the DC with the nTDSDA object referenced by cn!fromServer
447 # are in the same site, the KCC on dc deletes cn if all of
448 # the following are true:
450 # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
452 # No site settings object s exists for the local DC's site, or
453 # bit NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED is clear in
456 # Another nTDSConnection object cn2 exists such that cn and
457 # cn2 have the same parent object, cn!fromServer = cn2!fromServer,
460 # cn!whenCreated < cn2!whenCreated
462 # cn!whenCreated = cn2!whenCreated and
463 # cn!objectGUID < cn2!objectGUID
465 # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
467 if not cn_conn.is_generated():
470 if self.my_site.is_cleanup_ntdsconn_disabled():
473 # Loop thru connections looking for a duplicate that
474 # fulfills the previous criteria
476 packed_guid = ndr_pack(cn_conn.guid)
477 for cn2_conn in mydsa.connect_table.values():
478 if cn2_conn is cn_conn:
481 s2_dnstr = cn2_conn.get_from_dnstr()
483 # If the NTDS Connections has a different
484 # fromServer field then no match
485 if s2_dnstr != s_dnstr:
489 lesser = (cn_conn.whenCreated < cn2_conn.whenCreated or
490 (cn_conn.whenCreated == cn2_conn.whenCreated and
491 packed_guid < ndr_pack(cn2_conn.guid)))
496 if lesser and not cn_conn.is_rodc_topology():
497 cn_conn.to_be_deleted = True
499 # Given an nTDSConnection object cn, if the DC with the nTDSDSA
500 # object dc that is the parent object of cn and the DC with
501 # the nTDSDSA object referenced by cn!fromServer are in
502 # different sites, a KCC acting as an ISTG in dc's site
503 # deletes cn if all of the following are true:
505 # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
507 # cn!fromServer references an nTDSDSA object for a DC
508 # in a site other than the local DC's site.
510 # The keepConnections sequence returned by
511 # CreateIntersiteConnections() does not contain
512 # cn!objectGUID, or cn is "superseded by" (see below)
513 # another nTDSConnection cn2 and keepConnections
514 # contains cn2!objectGUID.
516 # The return value of CreateIntersiteConnections()
519 # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in
522 else: # different site
524 if not mydsa.is_istg():
527 if not cn_conn.is_generated():
531 # We are directly using this connection in intersite or
532 # we are using a connection which can supersede this one.
534 # MS-ADTS 6.2.2.4 - Removing Unnecessary Connections does not
535 # appear to be correct.
537 # 1. cn!fromServer and cn!parent appear inconsistent with
539 # 2. The repsFrom do not imply each other
541 if cn_conn in self.kept_connections: # and not_superceded:
544 # This is the result of create_intersite_connections
545 if not all_connected:
548 if not cn_conn.is_rodc_topology():
549 cn_conn.to_be_deleted = True
551 if mydsa.is_ro() or opts.readonly:
552 for connect in mydsa.connect_table.values():
553 if connect.to_be_deleted:
554 DEBUG_FN("TO BE DELETED:\n%s" % connect)
555 if connect.to_be_added:
556 DEBUG_FN("TO BE ADDED:\n%s" % connect)
558 # Peform deletion from our tables but perform
559 # no database modification
560 mydsa.commit_connections(self.samdb, ro=True)
562 # Commit any modified connections
563 mydsa.commit_connections(self.samdb)
565 def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
566 """Update an repsFrom object if required.
568 Part of MS-ADTS 6.2.2.5.
570 Update t_repsFrom if necessary to satisfy requirements. Such
571 updates are typically required when the IDL_DRSGetNCChanges
572 server has moved from one site to another--for example, to
573 enable compression when the server is moved from the
574 client's site to another site.
576 The repsFrom.update_flags bit field may be modified
577 auto-magically if any changes are made here. See
578 kcc_utils.RepsFromTo for gory details.
581 :param n_rep: NC replica we need
582 :param t_repsFrom: repsFrom tuple to modify
583 :param s_rep: NC replica at source DSA
584 :param s_dsa: source DSA
585 :param cn_conn: Local DSA NTDSConnection child
589 s_dnstr = s_dsa.dsa_dnstr
592 same_site = s_dnstr in self.my_site.dsa_table
594 # if schedule doesn't match then update and modify
595 times = convert_schedule_to_repltimes(cn_conn.schedule)
596 if times != t_repsFrom.schedule:
597 t_repsFrom.schedule = times
598 update |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
600 # Bit DRS_PER_SYNC is set in replicaFlags if and only
601 # if nTDSConnection schedule has a value v that specifies
602 # scheduled replication is to be performed at least once
604 if cn_conn.is_schedule_minimum_once_per_week():
606 if ((t_repsFrom.replica_flags &
607 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0):
608 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
610 # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
611 # if the source DSA and the local DC's nTDSDSA object are
612 # in the same site or source dsa is the FSMO role owner
613 # of one or more FSMO roles in the NC replica.
614 if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
616 if ((t_repsFrom.replica_flags &
617 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0):
618 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
620 # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
621 # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
622 # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
623 # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
624 # t.replicaFlags if and only if s and the local DC's
625 # nTDSDSA object are in different sites.
626 if ((cn_conn.options &
627 dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0):
629 if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
632 # it LOOKS as if this next test is a bit silly: it
633 # checks the flag then sets it if it not set; the same
634 # effect could be achieved by unconditionally setting
635 # it. But in fact the repsFrom object has special
636 # magic attached to it, and altering replica_flags has
637 # side-effects. That is bad in my opinion, but there
639 if ((t_repsFrom.replica_flags &
640 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
641 t_repsFrom.replica_flags |= \
642 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
646 if ((t_repsFrom.replica_flags &
647 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
648 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
650 # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
651 # and only if s and the local DC's nTDSDSA object are
652 # not in the same site and the
653 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
654 # clear in cn!options
655 if (not same_site and
657 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
659 if ((t_repsFrom.replica_flags &
660 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0):
661 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
663 # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
664 # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
665 if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
667 if ((t_repsFrom.replica_flags &
668 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0):
669 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
671 # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
672 # set in t.replicaFlags if and only if cn!enabledConnection = false.
673 if not cn_conn.is_enabled():
675 if ((t_repsFrom.replica_flags &
676 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0):
677 t_repsFrom.replica_flags |= \
678 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
680 if ((t_repsFrom.replica_flags &
681 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0):
682 t_repsFrom.replica_flags |= \
683 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
685 # If s and the local DC's nTDSDSA object are in the same site,
686 # cn!transportType has no value, or the RDN of cn!transportType
689 # Bit DRS_MAIL_REP in t.replicaFlags is clear.
691 # t.uuidTransport = NULL GUID.
693 # t.uuidDsa = The GUID-based DNS name of s.
697 # Bit DRS_MAIL_REP in t.replicaFlags is set.
699 # If x is the object with dsname cn!transportType,
700 # t.uuidTransport = x!objectGUID.
702 # Let a be the attribute identified by
703 # x!transportAddressAttribute. If a is
704 # the dNSHostName attribute, t.uuidDsa = the GUID-based
705 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
707 # It appears that the first statement i.e.
709 # "If s and the local DC's nTDSDSA object are in the same
710 # site, cn!transportType has no value, or the RDN of
711 # cn!transportType is CN=IP:"
713 # could be a slightly tighter statement if it had an "or"
714 # between each condition. I believe this should
717 # IF (same-site) OR (no-value) OR (type-ip)
719 # because IP should be the primary transport mechanism
720 # (even in inter-site) and the absense of the transportType
721 # attribute should always imply IP no matter if its multi-site
723 # NOTE MS-TECH INCORRECT:
725 # All indications point to these statements above being
726 # incorrectly stated:
728 # t.uuidDsa = The GUID-based DNS name of s.
730 # Let a be the attribute identified by
731 # x!transportAddressAttribute. If a is
732 # the dNSHostName attribute, t.uuidDsa = the GUID-based
733 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
735 # because the uuidDSA is a GUID and not a GUID-base DNS
736 # name. Nor can uuidDsa hold (s!parent)!a if not
737 # dNSHostName. What should have been said is:
739 # t.naDsa = The GUID-based DNS name of s
741 # That would also be correct if transportAddressAttribute
742 # were "mailAddress" because (naDsa) can also correctly
743 # hold the SMTP ISM service address.
745 nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
747 # We're not currently supporting SMTP replication
748 # so is_smtp_replication_available() is currently
749 # always returning False
751 cn_conn.transport_dnstr is None or
752 cn_conn.transport_dnstr.find("CN=IP") == 0 or
753 not is_smtp_replication_available())):
755 if ((t_repsFrom.replica_flags &
756 drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0):
757 t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
759 t_repsFrom.transport_guid = misc.GUID()
761 # See (NOTE MS-TECH INCORRECT) above
762 if t_repsFrom.version == 0x1:
763 if t_repsFrom.dns_name1 is None or \
764 t_repsFrom.dns_name1 != nastr:
765 t_repsFrom.dns_name1 = nastr
767 if t_repsFrom.dns_name1 is None or \
768 t_repsFrom.dns_name2 is None or \
769 t_repsFrom.dns_name1 != nastr or \
770 t_repsFrom.dns_name2 != nastr:
771 t_repsFrom.dns_name1 = nastr
772 t_repsFrom.dns_name2 = nastr
775 # XXX This entire branch is NEVER used! Because we don't do SMTP!
776 # (see the if condition above). Just close your eyes here.
777 if ((t_repsFrom.replica_flags &
778 drsuapi.DRSUAPI_DRS_MAIL_REP) == 0x0):
779 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_MAIL_REP
781 # We have a transport type but its not an
782 # object in the database
783 if cn_conn.transport_guid not in self.transport_table:
784 raise KCCError("Missing inter-site transport - (%s)" %
785 cn_conn.transport_dnstr)
787 x_transport = self.transport_table[str(cn_conn.transport_guid)]
789 if t_repsFrom.transport_guid != x_transport.guid:
790 t_repsFrom.transport_guid = x_transport.guid
792 # See (NOTE MS-TECH INCORRECT) above
793 if x_transport.address_attr == "dNSHostName":
795 if t_repsFrom.version == 0x1:
796 if t_repsFrom.dns_name1 is None or \
797 t_repsFrom.dns_name1 != nastr:
798 t_repsFrom.dns_name1 = nastr
800 if t_repsFrom.dns_name1 is None or \
801 t_repsFrom.dns_name2 is None or \
802 t_repsFrom.dns_name1 != nastr or \
803 t_repsFrom.dns_name2 != nastr:
804 t_repsFrom.dns_name1 = nastr
805 t_repsFrom.dns_name2 = nastr
808 # MS tech specification says we retrieve the named
809 # attribute in "transportAddressAttribute" from the parent of
812 pdnstr = s_dsa.get_parent_dnstr()
813 attrs = [x_transport.address_attr]
815 res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
817 except ldb.LdbError, (enum, estr):
819 "Unable to find attr (%s) for (%s) - (%s)" %
820 (x_transport.address_attr, pdnstr, estr))
823 nastr = str(msg[x_transport.address_attr][0])
825 # See (NOTE MS-TECH INCORRECT) above
826 if t_repsFrom.version == 0x1:
827 if t_repsFrom.dns_name1 is None or \
828 t_repsFrom.dns_name1 != nastr:
829 t_repsFrom.dns_name1 = nastr
831 if t_repsFrom.dns_name1 is None or \
832 t_repsFrom.dns_name2 is None or \
833 t_repsFrom.dns_name1 != nastr or \
834 t_repsFrom.dns_name2 != nastr:
836 t_repsFrom.dns_name1 = nastr
837 t_repsFrom.dns_name2 = nastr
839 if t_repsFrom.is_modified():
840 logger.debug("modify_repsFrom(): %s" % t_repsFrom)
842 def is_repsFrom_implied(self, n_rep, cn_conn):
843 """Given a NC replica and NTDS Connection, determine if the connection
844 implies a repsFrom tuple should be present from the source DSA listed
845 in the connection to the naming context
847 :param n_rep: NC replica
848 :param conn: NTDS Connection
849 ::returns (True || False), source DSA:
851 #XXX different conditions for "implies" than MS-ADTS 6.2.2
853 # NTDS Connection must satisfy all the following criteria
854 # to imply a repsFrom tuple is needed:
856 # cn!enabledConnection = true.
857 # cn!options does not contain NTDSCONN_OPT_RODC_TOPOLOGY.
858 # cn!fromServer references an nTDSDSA object.
862 if cn_conn.is_enabled() and not cn_conn.is_rodc_topology():
863 s_dnstr = cn_conn.get_from_dnstr()
864 if s_dnstr is not None:
865 s_dsa = self.get_dsa(s_dnstr)
867 # No DSA matching this source DN string?
871 # To imply a repsFrom tuple is needed, each of these
874 # An NC replica of the NC "is present" on the DC to
875 # which the nTDSDSA object referenced by cn!fromServer
878 # An NC replica of the NC "should be present" on
880 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
882 if s_rep is None or not s_rep.is_present():
885 # To imply a repsFrom tuple is needed, each of these
888 # The NC replica on the DC referenced by cn!fromServer is
889 # a writable replica or the NC replica that "should be
890 # present" on the local DC is a partial replica.
892 # The NC is not a domain NC, the NC replica that
893 # "should be present" on the local DC is a partial
894 # replica, cn!transportType has no value, or
895 # cn!transportType has an RDN of CN=IP.
897 implied = (not s_rep.is_ro() or n_rep.is_partial()) and \
898 (not n_rep.is_domain() or
899 n_rep.is_partial() or
900 cn_conn.transport_dnstr is None or
901 cn_conn.transport_dnstr.find("CN=IP") == 0)
908 def translate_ntdsconn(self, current_dsa=None):
909 """Adjust repsFrom to match NTDSConnections
911 This function adjusts values of repsFrom abstract attributes of NC
912 replicas on the local DC to match those implied by
913 nTDSConnection objects.
915 Based on [MS-ADTS] 6.2.2.5
917 :param current_dsa: optional DSA on whose behalf we are acting.
922 if current_dsa is None:
923 current_dsa = self.my_dsa
925 if current_dsa.is_translate_ntdsconn_disabled():
926 logger.debug("skipping translate_ntdsconn() "
927 "because disabling flag is set")
930 logger.debug("translate_ntdsconn(): enter")
932 current_rep_table, needed_rep_table = current_dsa.get_rep_tables()
934 # Filled in with replicas we currently have that need deleting
937 # We're using the MS notation names here to allow
938 # correlation back to the published algorithm.
940 # n_rep - NC replica (n)
941 # t_repsFrom - tuple (t) in n!repsFrom
942 # s_dsa - Source DSA of the replica. Defined as nTDSDSA
943 # object (s) such that (s!objectGUID = t.uuidDsa)
944 # In our IDL representation of repsFrom the (uuidDsa)
945 # attribute is called (source_dsa_obj_guid)
946 # cn_conn - (cn) is nTDSConnection object and child of the local
947 # DC's nTDSDSA object and (cn!fromServer = s)
948 # s_rep - source DSA replica of n
950 # If we have the replica and its not needed
951 # then we add it to the "to be deleted" list.
952 for dnstr in current_rep_table:
953 if dnstr not in needed_rep_table:
954 delete_reps.add(dnstr)
956 DEBUG_FN('current %d needed %d delete %d' % (len(current_rep_table),
957 len(needed_rep_table), len(delete_reps)))
960 DEBUG('deleting these reps: %s' % delete_reps)
961 for dnstr in delete_reps:
962 del current_rep_table[dnstr]
964 # Now perform the scan of replicas we'll need
965 # and compare any current repsFrom against the
967 for n_rep in needed_rep_table.values():
969 # load any repsFrom and fsmo roles as we'll
970 # need them during connection translation
971 n_rep.load_repsFrom(self.samdb)
972 n_rep.load_fsmo_roles(self.samdb)
974 # Loop thru the existing repsFrom tupples (if any)
975 # XXX This is a list and could contain duplicates
976 # (multiple load_repsFrom calls)
977 for t_repsFrom in n_rep.rep_repsFrom:
979 # for each tuple t in n!repsFrom, let s be the nTDSDSA
980 # object such that s!objectGUID = t.uuidDsa
981 guidstr = str(t_repsFrom.source_dsa_obj_guid)
982 s_dsa = self.get_dsa_by_guidstr(guidstr)
984 # Source dsa is gone from config (strange)
985 # so cleanup stale repsFrom for unlisted DSA
987 logger.warning("repsFrom source DSA guid (%s) not found" %
989 t_repsFrom.to_be_deleted = True
992 s_dnstr = s_dsa.dsa_dnstr
994 # Retrieve my DSAs connection object (if it exists)
995 # that specifies the fromServer equivalent to
996 # the DSA that is specified in the repsFrom source
997 connections = current_dsa.get_connection_by_from_dnstr(s_dnstr)
1002 for con in connections:
1003 if con.is_rodc_topology():
1007 # Let (cn) be the nTDSConnection object such that (cn)
1008 # is a child of the local DC's nTDSDSA object and
1009 # (cn!fromServer = s) and (cn!options) does not contain
1010 # NTDSCONN_OPT_RODC_TOPOLOGY or NULL if no such (cn) exists.
1012 # KCC removes this repsFrom tuple if any of the following
1017 #XXX varying possible interpretations of rodc_topology
1019 t_repsFrom.to_be_deleted = True
1022 # [...] KCC removes this repsFrom tuple if:
1024 # No NC replica of the NC "is present" on DSA that
1025 # would be source of replica
1027 # A writable replica of the NC "should be present" on
1028 # the local DC, but a partial replica "is present" on
1030 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
1032 if s_rep is None or not s_rep.is_present() or \
1033 (not n_rep.is_ro() and s_rep.is_partial()):
1035 t_repsFrom.to_be_deleted = True
1038 # If the KCC did not remove t from n!repsFrom, it updates t
1039 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
1041 # Loop thru connections and add implied repsFrom tuples
1042 # for each NTDSConnection under our local DSA if the
1043 # repsFrom is not already present
1044 for cn_conn in current_dsa.connect_table.values():
1046 implied, s_dsa = self.is_repsFrom_implied(n_rep, cn_conn)
1050 # Loop thru the existing repsFrom tupples (if any) and
1051 # if we already have a tuple for this connection then
1052 # no need to proceed to add. It will have been changed
1053 # to have the correct attributes above
1054 for t_repsFrom in n_rep.rep_repsFrom:
1055 guidstr = str(t_repsFrom.source_dsa_obj_guid)
1057 if s_dsa is self.get_dsa_by_guidstr(guidstr):
1064 # Create a new RepsFromTo and proceed to modify
1065 # it according to specification
1066 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
1068 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
1070 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
1072 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
1074 # Add to our NC repsFrom as this is newly computed
1075 if t_repsFrom.is_modified():
1076 n_rep.rep_repsFrom.append(t_repsFrom)
1079 # Display any to be deleted or modified repsFrom
1080 text = n_rep.dumpstr_to_be_deleted()
1082 logger.info("TO BE DELETED:\n%s" % text)
1083 text = n_rep.dumpstr_to_be_modified()
1085 logger.info("TO BE MODIFIED:\n%s" % text)
1087 # Peform deletion from our tables but perform
1088 # no database modification
1089 n_rep.commit_repsFrom(self.samdb, ro=True)
1091 # Commit any modified repsFrom to the NC replica
1092 n_rep.commit_repsFrom(self.samdb)
1094 def merge_failed_links(self):
1095 """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
1097 The KCC on a writable DC attempts to merge the link and connection
1098 failure information from bridgehead DCs in its own site to help it
1099 identify failed bridgehead DCs.
1101 Based on MS-ADTS 6.2.2.3.2 "Merge of kCCFailedLinks and kCCFailedLinks
1104 :param ping: An oracle of current bridgehead availability
1107 # 1. Queries every bridgehead server in your site (other than yourself)
1108 # 2. For every ntDSConnection that references a server in a different
1109 # site merge all the failure info
1111 # XXX - not implemented yet
1112 if opts.attempt_live_connections:
1113 DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1115 DEBUG_FN("skipping merge_failed_links() because it requires "
1116 "real network connections\n"
1117 "and we weren't asked to --attempt-live-connections")
1119 def setup_graph(self, part):
1120 """Set up an intersite graph
1122 An intersite graph has a Vertex for each site object, a
1123 MultiEdge for each SiteLink object, and a MutliEdgeSet for
1124 each siteLinkBridge object (or implied siteLinkBridge). It
1125 reflects the intersite topology in a slightly more abstract
1128 Roughly corresponds to MS-ADTS 6.2.2.3.4.3
1130 :param part: a Partition object
1131 :returns: an InterSiteGraph object
1135 g = IntersiteGraph()
1137 for site_guid, site in self.site_table.items():
1138 vertex = Vertex(site, part)
1139 vertex.guid = site_guid
1140 vertex.ndrpacked_guid = ndr_pack(site.site_guid)
1141 g.vertices.add(vertex)
1143 if not guid_to_vertex.get(site_guid):
1144 guid_to_vertex[site_guid] = []
1146 guid_to_vertex[site_guid].append(vertex)
1148 connected_vertices = set()
1149 for transport_guid, transport in self.transport_table.items():
1150 # Currently only ever "IP"
1151 if transport.name != 'IP':
1152 DEBUG_FN("setup_graph is ignoring transport %s" %
1155 for site_link_dn, site_link in self.sitelink_table.items():
1156 new_edge = create_edge(transport_guid, site_link,
1158 connected_vertices.update(new_edge.vertices)
1159 g.edges.add(new_edge)
1161 # If 'Bridge all site links' is enabled and Win2k3 bridges required
1163 # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1164 # No documentation for this however, ntdsapi.h appears to have:
1165 # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1166 if (((self.my_site.site_options & 0x00000002) == 0
1167 and (self.my_site.site_options & 0x00001000) == 0)):
1168 g.edge_set.add(create_auto_edge_set(g, transport_guid))
1170 # TODO get all site link bridges
1171 for site_link_bridge in []:
1172 g.edge_set.add(create_edge_set(g, transport_guid,
1175 g.connected_vertices = connected_vertices
1177 #be less verbose in dot file output unless --debug
1178 do_dot_files = opts.dot_files and opts.debug
1180 for edge in g.edges:
1181 for a, b in itertools.combinations(edge.vertices, 2):
1182 dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1183 verify_properties = ()
1184 verify_and_dot('site_edges', dot_edges, directed=False,
1185 label=self.my_dsa_dnstr,
1186 properties=verify_properties, debug=DEBUG,
1188 dot_files=do_dot_files)
1192 def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1193 """Get a bridghead DC for a site.
1195 Part of MS-ADTS 6.2.2.3.4.4
1197 :param site: site object representing for which a bridgehead
1199 :param part: crossRef for NC to replicate.
1200 :param transport: interSiteTransport object for replication
1202 :param partial_ok: True if a DC containing a partial
1203 replica or a full replica will suffice, False if only
1204 a full replica will suffice.
1205 :param detect_failed: True to detect failed DCs and route
1206 replication traffic around them, False to assume no DC
1208 :return: dsa object for the bridgehead DC or None
1211 bhs = self.get_all_bridgeheads(site, part, transport,
1212 partial_ok, detect_failed)
1214 DEBUG_MAGENTA("get_bridgehead:\n\tsitedn=%s\n\tbhdn=None" %
1218 DEBUG_GREEN("get_bridgehead:\n\tsitedn=%s\n\tbhdn=%s" %
1219 (site.site_dnstr, bhs[0].dsa_dnstr))
1222 def get_all_bridgeheads(self, site, part, transport,
1223 partial_ok, detect_failed):
1224 """Get all bridghead DCs on a site satisfying the given criteria
1226 Part of MS-ADTS 6.2.2.3.4.4
1228 :param site: site object representing the site for which
1229 bridgehead DCs are desired.
1230 :param part: partition for NC to replicate.
1231 :param transport: interSiteTransport object for
1232 replication traffic.
1233 :param partial_ok: True if a DC containing a partial
1234 replica or a full replica will suffice, False if
1235 only a full replica will suffice.
1236 :param detect_failed: True to detect failed DCs and route
1237 replication traffic around them, FALSE to assume
1239 :return: list of dsa object for available bridgehead DCs
1244 logger.debug("get_all_bridgeheads: %s" % transport.name)
1245 if 'Site-5' in site.site_dnstr:
1246 DEBUG_RED("get_all_bridgeheads with %s, part%s, partial_ok %s"
1247 " detect_failed %s" % (site.site_dnstr, part.partstr,
1248 partial_ok, detect_failed))
1249 logger.debug(site.rw_dsa_table)
1250 for dsa in site.rw_dsa_table.values():
1252 pdnstr = dsa.get_parent_dnstr()
1254 # IF t!bridgeheadServerListBL has one or more values and
1255 # t!bridgeheadServerListBL does not contain a reference
1256 # to the parent object of dc then skip dc
1257 if ((len(transport.bridgehead_list) != 0 and
1258 pdnstr not in transport.bridgehead_list)):
1261 # IF dc is in the same site as the local DC
1262 # IF a replica of cr!nCName is not in the set of NC replicas
1263 # that "should be present" on dc or a partial replica of the
1264 # NC "should be present" but partialReplicasOkay = FALSE
1266 if self.my_site.same_site(dsa):
1267 needed, ro, partial = part.should_be_present(dsa)
1268 if not needed or (partial and not partial_ok):
1270 rep = dsa.get_current_replica(part.nc_dnstr)
1273 # IF an NC replica of cr!nCName is not in the set of NC
1274 # replicas that "are present" on dc or a partial replica of
1275 # the NC "is present" but partialReplicasOkay = FALSE
1278 rep = dsa.get_current_replica(part.nc_dnstr)
1279 if rep is None or (rep.is_partial() and not partial_ok):
1282 # IF AmIRODC() and cr!nCName corresponds to default NC then
1283 # Let dsaobj be the nTDSDSA object of the dc
1284 # IF dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1286 if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1287 if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1290 # IF t!name != "IP" and the parent object of dc has no value for
1291 # the attribute specified by t!transportAddressAttribute
1293 if transport.name != "IP":
1294 # MS tech specification says we retrieve the named
1295 # attribute in "transportAddressAttribute" from the parent
1298 attrs = [transport.address_attr]
1300 res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
1302 except ldb.LdbError, (enum, estr):
1306 if transport.address_attr not in msg:
1308 #XXX nastr is NEVER USED. It will be removed.
1309 nastr = str(msg[transport.address_attr][0])
1311 # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1313 if self.is_bridgehead_failed(dsa, detect_failed):
1314 DEBUG("bridgehead is failed")
1317 logger.debug("get_all_bridgeheads: dsadn=%s" % dsa.dsa_dnstr)
1320 # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1322 # SORT bhs such that all GC servers precede DCs that are not GC
1323 # servers, and otherwise by ascending objectGUID
1325 # SORT bhs in a random order
1326 if site.is_random_bridgehead_disabled():
1327 bhs.sort(sort_dsa_by_gc_and_guid)
1333 def is_bridgehead_failed(self, dsa, detect_failed):
1334 """Determine whether a given DC is known to be in a failed state
1335 ::returns: True if and only if the DC should be considered failed
1337 Here we DEPART from the pseudo code spec which appears to be
1338 wrong. It says, in full:
1340 /***** BridgeheadDCFailed *****/
1341 /* Determine whether a given DC is known to be in a failed state.
1342 * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1343 * IN: detectFailedDCs - TRUE if and only failed DC detection is
1345 * RETURNS: TRUE if and only if the DC should be considered to be in a
1348 BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1350 IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1351 the options attribute of the site settings object for the local
1354 ELSEIF a tuple z exists in the kCCFailedLinks or
1355 kCCFailedConnections variables such that z.UUIDDsa =
1356 objectGUID, z.FailureCount > 1, and the current time -
1357 z.TimeFirstFailure > 2 hours
1360 RETURN detectFailedDCs
1364 where you will see detectFailedDCs is not behaving as
1365 advertised -- it is acting as a default return code in the
1366 event that a failure is not detected, not a switch turning
1367 detection on or off. Elsewhere the documentation seems to
1368 concur with the comment rather than the code.
1370 if not detect_failed:
1373 # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1374 # When DETECT_STALE_DISABLED, we can never know of if
1375 # it's in a failed state
1376 if self.my_site.site_options & 0x00000008:
1379 return self.is_stale_link_connection(dsa)
1381 def create_connection(self, part, rbh, rsite, transport,
1382 lbh, lsite, link_opt, link_sched,
1383 partial_ok, detect_failed):
1384 """Create an nTDSConnection object with the given parameters
1385 if one does not already exist.
1387 :param part: crossRef object for the NC to replicate.
1388 :param rbh: nTDSDSA object for DC to act as the
1389 IDL_DRSGetNCChanges server (which is in a site other
1390 than the local DC's site).
1391 :param rsite: site of the rbh
1392 :param transport: interSiteTransport object for the transport
1393 to use for replication traffic.
1394 :param lbh: nTDSDSA object for DC to act as the
1395 IDL_DRSGetNCChanges client (which is in the local DC's site).
1396 :param lsite: site of the lbh
1397 :param link_opt: Replication parameters (aggregated siteLink options,
1399 :param link_sched: Schedule specifying the times at which
1400 to begin replicating.
1401 :partial_ok: True if bridgehead DCs containing partial
1402 replicas of the NC are acceptable.
1403 :param detect_failed: True to detect failed DCs and route
1404 replication traffic around them, FALSE to assume no DC
1407 rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1409 rbh_table = {x.dsa_dnstr: x for x in rbhs_all}
1411 DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all),
1412 [x.dsa_dnstr for x in rbhs_all]))
1414 # MS-TECH says to compute rbhs_avail but then doesn't use it
1415 # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1416 # partial_ok, detect_failed)
1418 lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1421 lbhs_all.append(lbh)
1423 DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all),
1424 [x.dsa_dnstr for x in lbhs_all]))
1426 # MS-TECH says to compute lbhs_avail but then doesn't use it
1427 # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1428 # partial_ok, detect_failed)
1430 # FOR each nTDSConnection object cn such that the parent of cn is
1431 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1432 for ldsa in lbhs_all:
1433 for cn in ldsa.connect_table.values():
1435 rdsa = rbh_table.get(cn.from_dnstr)
1439 DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1440 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1441 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1442 # cn!transportType references t
1443 if ((cn.is_generated() and
1444 not cn.is_rodc_topology() and
1445 cn.transport_guid == transport.guid)):
1447 # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1448 # cn!options and cn!schedule != sch
1449 # Perform an originating update to set cn!schedule to
1451 if ((not cn.is_user_owned_schedule() and
1452 not cn.is_equivalent_schedule(link_sched))):
1453 cn.schedule = link_sched
1454 cn.set_modified(True)
1456 # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1457 # NTDSCONN_OPT_USE_NOTIFY are set in cn
1458 if cn.is_override_notify_default() and \
1461 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1463 # Perform an originating update to clear bits
1464 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1465 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1466 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1468 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1469 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1470 cn.set_modified(True)
1475 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1477 # Perform an originating update to set bits
1478 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1479 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1480 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1482 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1483 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1484 cn.set_modified(True)
1486 # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1487 if cn.is_twoway_sync():
1489 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1491 # Perform an originating update to clear bit
1492 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1493 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1494 cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1495 cn.set_modified(True)
1500 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1502 # Perform an originating update to set bit
1503 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1504 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1505 cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1506 cn.set_modified(True)
1508 # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1510 if cn.is_intersite_compression_disabled():
1512 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1514 # Perform an originating update to clear bit
1515 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1518 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0):
1520 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1521 cn.set_modified(True)
1525 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1527 # Perform an originating update to set bit
1528 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1531 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1533 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1534 cn.set_modified(True)
1536 # Display any modified connection
1538 if cn.to_be_modified:
1539 logger.info("TO BE MODIFIED:\n%s" % cn)
1541 ldsa.commit_connections(self.samdb, ro=True)
1543 ldsa.commit_connections(self.samdb)
1546 valid_connections = 0
1548 # FOR each nTDSConnection object cn such that cn!parent is
1549 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1550 for ldsa in lbhs_all:
1551 for cn in ldsa.connect_table.values():
1553 rdsa = rbh_table.get(cn.from_dnstr)
1557 DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1559 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1560 # cn!transportType references t) and
1561 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1562 if (((not cn.is_generated() or
1563 cn.transport_guid == transport.guid) and
1564 not cn.is_rodc_topology())):
1566 # LET rguid be the objectGUID of the nTDSDSA object
1567 # referenced by cn!fromServer
1568 # LET lguid be (cn!parent)!objectGUID
1570 # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1571 # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1572 # Increment cValidConnections by 1
1573 if ((not self.is_bridgehead_failed(rdsa, detect_failed) and
1574 not self.is_bridgehead_failed(ldsa, detect_failed))):
1575 valid_connections += 1
1577 # IF keepConnections does not contain cn!objectGUID
1578 # APPEND cn!objectGUID to keepConnections
1579 self.kept_connections.add(cn)
1582 DEBUG_RED("valid connections %d" % valid_connections)
1583 DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1584 # IF cValidConnections = 0
1585 if valid_connections == 0:
1587 # LET opt be NTDSCONN_OPT_IS_GENERATED
1588 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1590 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1591 # SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1592 # NTDSCONN_OPT_USE_NOTIFY in opt
1593 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1594 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1595 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1597 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1598 # SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1599 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1600 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1602 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1604 # SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1606 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1607 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1609 # Perform an originating update to create a new nTDSConnection
1610 # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1611 # cn!options = opt, cn!transportType is a reference to t,
1612 # cn!fromServer is a reference to rbh, and cn!schedule = sch
1613 DEBUG_FN("new connection, KCC dsa: %s" % self.my_dsa.dsa_dnstr)
1614 cn = lbh.new_connection(opt, 0, transport,
1615 rbh.dsa_dnstr, link_sched)
1617 # Display any added connection
1620 logger.info("TO BE ADDED:\n%s" % cn)
1622 lbh.commit_connections(self.samdb, ro=True)
1624 lbh.commit_connections(self.samdb)
1626 # APPEND cn!objectGUID to keepConnections
1627 self.kept_connections.add(cn)
1629 def add_transports(self, vertex, local_vertex, graph, detect_failed):
1630 """Build a Vertex's transport lists
1632 Each vertex has accept_red_red and accept_black lists that
1633 list what transports they accept under various conditions. The
1634 only transport that is ever accepted is IP, and a dummy extra
1635 transport called "EDGE_TYPE_ALL".
1637 Part of MS-ADTS 6.2.2.3.4.3 -- ColorVertices
1639 :param vertex: the remote vertex we are thinking about
1640 :param local_vertex: the vertex relating to the local site.
1641 :param graph: the intersite graph
1642 :param detect_failed: whether to detect failed links
1643 :return: True if some bridgeheads were not found
1645 # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1646 # here, but using vertex seems to make more sense. That is,
1647 # the docs want this:
1649 #bh = self.get_bridgehead(vertex.site, vertex.part, transport,
1650 # local_vertex.is_black(), detect_failed)
1654 vertex.accept_red_red = []
1655 vertex.accept_black = []
1656 found_failed = False
1657 for t_guid, transport in self.transport_table.items():
1658 if transport.name != 'IP':
1659 #XXX well this is cheating a bit
1660 logging.warning("WARNING: we are ignoring a transport named %r"
1664 # FLAG_CR_NTDS_DOMAIN 0x00000002
1665 if ((vertex.is_red() and transport.name != "IP" and
1666 vertex.part.system_flags & 0x00000002)):
1669 if vertex not in graph.connected_vertices:
1672 partial_replica_okay = vertex.is_black()
1673 bh = self.get_bridgehead(vertex.site, vertex.part, transport,
1674 partial_replica_okay, detect_failed)
1679 vertex.accept_red_red.append(t_guid)
1680 vertex.accept_black.append(t_guid)
1682 # Add additional transport to allow another run of Dijkstra
1683 vertex.accept_red_red.append("EDGE_TYPE_ALL")
1684 vertex.accept_black.append("EDGE_TYPE_ALL")
1688 def create_connections(self, graph, part, detect_failed):
1689 """Construct an NC replica graph for the NC identified by
1690 the given crossRef, then create any additional nTDSConnection
1693 :param graph: site graph.
1694 :param part: crossRef object for NC.
1695 :param detect_failed: True to detect failed DCs and route
1696 replication traffic around them, False to assume no DC
1699 Modifies self.kept_connections by adding any connections
1700 deemed to be "in use".
1702 ::returns: (all_connected, found_failed_dc)
1703 (all_connected) True if the resulting NC replica graph
1704 connects all sites that need to be connected.
1705 (found_failed_dc) True if one or more failed DCs were
1708 all_connected = True
1709 found_failed = False
1711 logger.debug("create_connections(): enter\n"
1712 "\tpartdn=%s\n\tdetect_failed=%s" %
1713 (part.nc_dnstr, detect_failed))
1715 # XXX - This is a highly abbreviated function from the MS-TECH
1716 # ref. It creates connections between bridgeheads to all
1717 # sites that have appropriate replicas. Thus we are not
1718 # creating a minimum cost spanning tree but instead
1719 # producing a fully connected tree. This should produce
1720 # a full (albeit not optimal cost) replication topology.
1722 my_vertex = Vertex(self.my_site, part)
1723 my_vertex.color_vertex()
1725 for v in graph.vertices:
1727 if self.add_transports(v, my_vertex, graph, False):
1730 # No NC replicas for this NC in the site of the local DC,
1731 # so no nTDSConnection objects need be created
1732 if my_vertex.is_white():
1733 return all_connected, found_failed
1735 edge_list, n_components = get_spanning_tree_edges(graph,
1739 logger.debug("%s Number of components: %d" %
1740 (part.nc_dnstr, n_components))
1741 if n_components > 1:
1742 all_connected = False
1744 # LET partialReplicaOkay be TRUE if and only if
1745 # localSiteVertex.Color = COLOR.BLACK
1746 partial_ok = my_vertex.is_black()
1748 # Utilize the IP transport only for now
1749 transport = self.ip_transport
1751 DEBUG("edge_list %s" % edge_list)
1753 # XXX more accurate comparison?
1754 if e.directed and e.vertices[0].site is self.my_site:
1757 if e.vertices[0].site is self.my_site:
1758 rsite = e.vertices[1].site
1760 rsite = e.vertices[0].site
1762 # We don't make connections to our own site as that
1763 # is intrasite topology generator's job
1764 if rsite is self.my_site:
1765 DEBUG("rsite is my_site")
1768 # Determine bridgehead server in remote site
1769 rbh = self.get_bridgehead(rsite, part, transport,
1770 partial_ok, detect_failed)
1774 # RODC acts as an BH for itself
1776 # LET lbh be the nTDSDSA object of the local DC
1778 # LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1779 # cr, t, partialReplicaOkay, detectFailedDCs)
1780 if self.my_dsa.is_ro():
1781 lsite = self.my_site
1784 lsite = self.my_site
1785 lbh = self.get_bridgehead(lsite, part, transport,
1786 partial_ok, detect_failed)
1789 DEBUG_RED("DISASTER! lbh is None")
1794 DEBUG_BLUE("vertices")
1796 DEBUG_BLUE("bridgeheads")
1798 DEBUG_BLUE("-" * 70)
1800 sitelink = e.site_link
1801 if sitelink is None:
1805 link_opt = sitelink.options
1806 link_sched = sitelink.schedule
1808 self.create_connection(part, rbh, rsite, transport,
1809 lbh, lsite, link_opt, link_sched,
1810 partial_ok, detect_failed)
1812 return all_connected, found_failed
1814 def create_intersite_connections(self):
1815 """Computes an NC replica graph for each NC replica that "should be
1816 present" on the local DC or "is present" on any DC in the same site
1817 as the local DC. For each edge directed to an NC replica on such a
1818 DC from an NC replica on a DC in another site, the KCC creates an
1819 nTDSConnection object to imply that edge if one does not already
1822 Modifies self.kept_connections - A set of nTDSConnection
1823 objects for edges that are directed
1824 to the local DC's site in one or more NC replica graphs.
1826 returns: True if spanning trees were created for all NC replica
1827 graphs, otherwise False.
1829 all_connected = True
1830 self.kept_connections = set()
1832 # LET crossRefList be the set containing each object o of class
1833 # crossRef such that o is a child of the CN=Partitions child of the
1836 # FOR each crossRef object cr in crossRefList
1837 # IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1838 # is clear in cr!systemFlags, skip cr.
1839 # LET g be the GRAPH return of SetupGraph()
1841 for part in self.part_table.values():
1843 if not part.is_enabled():
1846 if part.is_foreign():
1849 graph = self.setup_graph(part)
1851 # Create nTDSConnection objects, routing replication traffic
1852 # around "failed" DCs.
1853 found_failed = False
1855 connected, found_failed = self.create_connections(graph,
1858 DEBUG("with detect_failed: connected %s Found failed %s" %
1859 (connected, found_failed))
1861 all_connected = False
1864 # One or more failed DCs preclude use of the ideal NC
1865 # replica graph. Add connections for the ideal graph.
1866 self.create_connections(graph, part, False)
1868 return all_connected
1870 def intersite(self):
1871 """The head method for generating the inter-site KCC replica
1872 connection graph and attendant nTDSConnection objects
1875 Produces self.kept_connections set of NTDS Connections
1876 that should be kept during subsequent pruning process.
1878 ::return (True or False): (True) if the produced NC replica
1879 graph connects all sites that need to be connected
1884 mysite = self.my_site
1885 all_connected = True
1887 logger.debug("intersite(): enter")
1889 # Determine who is the ISTG
1891 mysite.select_istg(self.samdb, mydsa, ro=True)
1893 mysite.select_istg(self.samdb, mydsa, ro=False)
1895 # Test whether local site has topology disabled
1896 if mysite.is_intersite_topology_disabled():
1897 logger.debug("intersite(): exit disabled all_connected=%d" %
1899 return all_connected
1901 if not mydsa.is_istg():
1902 logger.debug("intersite(): exit not istg all_connected=%d" %
1904 return all_connected
1906 self.merge_failed_links()
1908 # For each NC with an NC replica that "should be present" on the
1909 # local DC or "is present" on any DC in the same site as the
1910 # local DC, the KCC constructs a site graph--a precursor to an NC
1911 # replica graph. The site connectivity for a site graph is defined
1912 # by objects of class interSiteTransport, siteLink, and
1913 # siteLinkBridge in the config NC.
1915 all_connected = self.create_intersite_connections()
1917 logger.debug("intersite(): exit all_connected=%d" % all_connected)
1918 return all_connected
1920 def update_rodc_connection(self):
1921 """Runs when the local DC is an RODC and updates the RODC NTFRS
1924 # Given an nTDSConnection object cn1, such that cn1.options contains
1925 # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1926 # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1927 # that the following is true:
1929 # cn1.fromServer = cn2.fromServer
1930 # cn1.schedule = cn2.schedule
1932 # If no such cn2 can be found, cn1 is not modified.
1933 # If no such cn1 can be found, nothing is modified by this task.
1935 if not self.my_dsa.is_ro():
1938 all_connections = self.my_dsa.connect_table.values()
1939 ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1940 rw_connections = [x for x in all_connections
1941 if x not in ro_connections]
1943 # XXX here we are dealing with multiple RODC_TOPO connections,
1944 # if they exist. It is not clear whether the spec means that
1945 # or if it ever arises.
1946 if rw_connections and ro_connections:
1947 for con in ro_connections:
1948 cn2 = rw_connections[0]
1949 con.from_dnstr = cn2.from_dnstr
1950 con.schedule = cn2.schedule
1951 con.to_be_modified = True
1953 self.my_dsa.commit_connections(self.samdb, ro=opts.readonly)
1955 def intrasite_max_node_edges(self, node_count):
1956 """Returns the maximum number of edges directed to a node in
1957 the intrasite replica graph.
1959 The KCC does not create more
1960 than 50 edges directed to a single DC. To optimize replication,
1961 we compute that each node should have n+2 total edges directed
1962 to it such that (n) is the smallest non-negative integer
1963 satisfying (node_count <= 2*(n*n) + 6*n + 7)
1965 (If the number of edges is m (i.e. n + 2), that is the same as
1966 2 * m*m - 2 * m + 3).
1976 :param node_count: total number of nodes in the replica graph
1978 The intention is that there should be no more than 3 hops
1979 between any two DSAs at a site. With up to 7 nodes the 2 edges
1980 of the ring are enough; any configuration of extra edges with
1981 8 nodes will be enough. It is less clear that the 3 hop
1982 guarantee holds at e.g. 15 nodes in degenerate cases, but
1983 those are quite unlikely given the extra edges are randomly
1988 if node_count <= (2 * (n * n) + (6 * n) + 7):
1996 def construct_intrasite_graph(self, site_local, dc_local,
1997 nc_x, gc_only, detect_stale):
1999 # We're using the MS notation names here to allow
2000 # correlation back to the published algorithm.
2002 # nc_x - naming context (x) that we are testing if it
2003 # "should be present" on the local DC
2004 # f_of_x - replica (f) found on a DC (s) for NC (x)
2005 # dc_s - DC where f_of_x replica was found
2006 # dc_local - local DC that potentially needs a replica
2008 # r_list - replica list R
2009 # p_of_x - replica (p) is partial and found on a DC (s)
2011 # l_of_x - replica (l) is the local replica for NC (x)
2012 # that should appear on the local DC
2013 # r_len = is length of replica list |R|
2015 # If the DSA doesn't need a replica for this
2016 # partition (NC x) then continue
2017 needed, ro, partial = nc_x.should_be_present(dc_local)
2019 DEBUG_YELLOW("construct_intrasite_graph(): enter" +
2020 "\n\tgc_only=%d" % gc_only +
2021 "\n\tdetect_stale=%d" % detect_stale +
2022 "\n\tneeded=%s" % needed +
2024 "\n\tpartial=%s" % partial +
2028 DEBUG_RED("%s lacks 'should be present' status, "
2029 "aborting construct_intersite_graph!" %
2033 # Create a NCReplica that matches what the local replica
2034 # should say. We'll use this below in our r_list
2035 l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid,
2038 l_of_x.identify_by_basedn(self.samdb)
2040 l_of_x.rep_partial = partial
2043 # Add this replica that "should be present" to the
2044 # needed replica table for this DSA
2045 dc_local.add_needed_replica(l_of_x)
2049 # Let R be a sequence containing each writable replica f of x
2050 # such that f "is present" on a DC s satisfying the following
2053 # * s is a writable DC other than the local DC.
2055 # * s is in the same site as the local DC.
2057 # * If x is a read-only full replica and x is a domain NC,
2058 # then the DC's functional level is at least
2059 # DS_BEHAVIOR_WIN2008.
2061 # * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
2062 # in the options attribute of the site settings object for
2063 # the local DC's site, or no tuple z exists in the
2064 # kCCFailedLinks or kCCFailedConnections variables such
2065 # that z.UUIDDsa is the objectGUID of the nTDSDSA object
2066 # for s, z.FailureCount > 0, and the current time -
2067 # z.TimeFirstFailure > 2 hours.
2071 # We'll loop thru all the DSAs looking for
2072 # writeable NC replicas that match the naming
2073 # context dn for (nc_x)
2075 for dc_s in self.my_site.dsa_table.values():
2076 # If this partition (nc_x) doesn't appear as a
2077 # replica (f_of_x) on (dc_s) then continue
2078 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2081 # Pull out the NCReplica (f) of (x) with the dn
2082 # that matches NC (x) we are examining.
2083 f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2085 # Replica (f) of NC (x) must be writable
2089 # Replica (f) of NC (x) must satisfy the
2090 # "is present" criteria for DC (s) that
2092 if not f_of_x.is_present():
2095 # DC (s) must be a writable DSA other than
2096 # my local DC. In other words we'd only replicate
2097 # from other writable DC
2098 if dc_s.is_ro() or dc_s is dc_local:
2101 # Certain replica graphs are produced only
2102 # for global catalogs, so test against
2103 # method input parameter
2104 if gc_only and not dc_s.is_gc():
2107 # DC (s) must be in the same site as the local DC
2108 # as this is the intra-site algorithm. This is
2109 # handled by virtue of placing DSAs in per
2110 # site objects (see enclosing for() loop)
2112 # If NC (x) is intended to be read-only full replica
2113 # for a domain NC on the target DC then the source
2114 # DC should have functional level at minimum WIN2008
2116 # Effectively we're saying that in order to replicate
2117 # to a targeted RODC (which was introduced in Windows 2008)
2118 # then we have to replicate from a DC that is also minimally
2121 # You can also see this requirement in the MS special
2122 # considerations for RODC which state that to deploy
2123 # an RODC, at least one writable domain controller in
2124 # the domain must be running Windows Server 2008
2125 if ro and not partial and nc_x.nc_type == NCType.domain:
2126 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2129 # If we haven't been told to turn off stale connection
2130 # detection and this dsa has a stale connection then
2132 if detect_stale and self.is_stale_link_connection(dc_s):
2135 # Replica meets criteria. Add it to table indexed
2136 # by the GUID of the DC that it appears on
2137 r_list.append(f_of_x)
2139 # If a partial (not full) replica of NC (x) "should be present"
2140 # on the local DC, append to R each partial replica (p of x)
2141 # such that p "is present" on a DC satisfying the same
2142 # criteria defined above for full replica DCs.
2144 # XXX This loop and the previous one differ only in whether
2145 # the replica is partial or not. here we only accept partial
2146 # (because we're partial); before we only accepted full. Order
2147 # doen't matter (the list is sorted a few lines down) so these
2148 # loops could easily be merged. Or this could be a helper
2152 # Now we loop thru all the DSAs looking for
2153 # partial NC replicas that match the naming
2154 # context dn for (NC x)
2155 for dc_s in self.my_site.dsa_table.values():
2157 # If this partition NC (x) doesn't appear as a
2158 # replica (p) of NC (x) on the dsa DC (s) then
2160 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2163 # Pull out the NCReplica with the dn that
2164 # matches NC (x) we are examining.
2165 p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2167 # Replica (p) of NC (x) must be partial
2168 if not p_of_x.is_partial():
2171 # Replica (p) of NC (x) must satisfy the
2172 # "is present" criteria for DC (s) that
2174 if not p_of_x.is_present():
2177 # DC (s) must be a writable DSA other than
2178 # my DSA. In other words we'd only replicate
2179 # from other writable DSA
2180 if dc_s.is_ro() or dc_s is dc_local:
2183 # Certain replica graphs are produced only
2184 # for global catalogs, so test against
2185 # method input parameter
2186 if gc_only and not dc_s.is_gc():
2189 # If we haven't been told to turn off stale connection
2190 # detection and this dsa has a stale connection then
2192 if detect_stale and self.is_stale_link_connection(dc_s):
2195 # Replica meets criteria. Add it to table indexed
2196 # by the GUID of the DSA that it appears on
2197 r_list.append(p_of_x)
2199 # Append to R the NC replica that "should be present"
2201 r_list.append(l_of_x)
2203 r_list.sort(sort_replica_by_dsa_guid)
2206 max_node_edges = self.intrasite_max_node_edges(r_len)
2208 # Add a node for each r_list element to the replica graph
2211 node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2212 graph_list.append(node)
2214 # For each r(i) from (0 <= i < |R|-1)
2216 while i < (r_len-1):
2217 # Add an edge from r(i) to r(i+1) if r(i) is a full
2218 # replica or r(i+1) is a partial replica
2219 if not r_list[i].is_partial() or r_list[i+1].is_partial():
2220 graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
2222 # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2223 # replica or ri is a partial replica.
2224 if not r_list[i+1].is_partial() or r_list[i].is_partial():
2225 graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
2228 # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2229 # or r0 is a partial replica.
2230 if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
2231 graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
2233 # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2234 # r|R|-1 is a partial replica.
2235 if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
2236 graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
2238 DEBUG("r_list is length %s" % len(r_list))
2239 DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr))
2242 do_dot_files = opts.dot_files and opts.debug
2243 if opts.verify or do_dot_files:
2245 dot_vertices = set()
2246 for v1 in graph_list:
2247 dot_vertices.add(v1.dsa_dnstr)
2248 for v2 in v1.edge_from:
2249 dot_edges.append((v2, v1.dsa_dnstr))
2250 dot_vertices.add(v2)
2252 verify_properties = ('connected', 'directed_double_ring_or_small')
2253 verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2254 label='%s__%s__%s' % (site_local.site_dnstr,
2255 nctype_lut[nc_x.nc_type],
2257 properties=verify_properties, debug=DEBUG,
2259 dot_files=do_dot_files, directed=True)
2261 # For each existing nTDSConnection object implying an edge
2262 # from rj of R to ri such that j != i, an edge from rj to ri
2263 # is not already in the graph, and the total edges directed
2264 # to ri is less than n+2, the KCC adds that edge to the graph.
2265 for vertex in graph_list:
2266 dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2267 for connect in dsa.connect_table.values():
2268 remote = connect.from_dnstr
2269 if remote in self.my_site.dsa_table:
2270 vertex.add_edge_from(remote)
2272 DEBUG('reps are: %s' % ' '.join(x.rep_dsa_dnstr for x in r_list))
2273 DEBUG('dsas are: %s' % ' '.join(x.dsa_dnstr for x in graph_list))
2275 for tnode in graph_list:
2276 # To optimize replication latency in sites with many NC
2277 # replicas, the KCC adds new edges directed to ri to bring
2278 # the total edges to n+2, where the NC replica rk of R
2279 # from which the edge is directed is chosen at random such
2280 # that k != i and an edge from rk to ri is not already in
2283 # Note that the KCC tech ref does not give a number for
2284 # the definition of "sites with many NC replicas". At a
2285 # bare minimum to satisfy n+2 edges directed at a node we
2286 # have to have at least three replicas in |R| (i.e. if n
2287 # is zero then at least replicas from two other graph
2288 # nodes may direct edges to us).
2289 if r_len >= 3 and not tnode.has_sufficient_edges():
2290 candidates = [x for x in graph_list if
2292 x.dsa_dnstr not in tnode.edge_from)]
2294 DEBUG_BLUE("looking for random link for %s. r_len %d, "
2295 "graph len %d candidates %d"
2296 % (tnode.dsa_dnstr, r_len, len(graph_list),
2299 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2301 while candidates and not tnode.has_sufficient_edges():
2302 other = random.choice(candidates)
2303 DEBUG("trying to add candidate %s" % other.dsa_dstr)
2304 if not tnode.add_edge_from(other):
2305 DEBUG_RED("could not add %s" % other.dsa_dstr)
2306 candidates.remove(other)
2308 DEBUG_FN("not adding links to %s: nodes %s, links is %s/%s" %
2309 (tnode.dsa_dnstr, r_len, len(tnode.edge_from),
2312 # Print the graph node in debug mode
2313 logger.debug("%s" % tnode)
2315 # For each edge directed to the local DC, ensure a nTDSConnection
2316 # points to us that satisfies the KCC criteria
2318 if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2319 tnode.add_connections_from_edges(dc_local)
2321 if opts.verify or do_dot_files:
2323 dot_vertices = set()
2324 for v1 in graph_list:
2325 dot_vertices.add(v1.dsa_dnstr)
2326 for v2 in v1.edge_from:
2327 dot_edges.append((v2, v1.dsa_dnstr))
2328 dot_vertices.add(v2)
2330 verify_properties = ('connected', 'directed_double_ring_or_small')
2331 verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2332 label='%s__%s__%s' % (site_local.site_dnstr,
2333 nctype_lut[nc_x.nc_type],
2335 properties=verify_properties, debug=DEBUG,
2337 dot_files=do_dot_files, directed=True)
2339 def intrasite(self):
2340 """The head method for generating the intra-site KCC replica
2341 connection graph and attendant nTDSConnection objects
2347 logger.debug("intrasite(): enter")
2349 # Test whether local site has topology disabled
2350 mysite = self.my_site
2351 if mysite.is_intrasite_topology_disabled():
2354 detect_stale = (not mysite.is_detect_stale_disabled())
2355 for connect in mydsa.connect_table.values():
2356 if connect.to_be_added:
2357 DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2359 # Loop thru all the partitions, with gc_only False
2360 for partdn, part in self.part_table.items():
2361 self.construct_intrasite_graph(mysite, mydsa, part, False,
2363 for connect in mydsa.connect_table.values():
2364 if connect.to_be_added:
2365 DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2367 # If the DC is a GC server, the KCC constructs an additional NC
2368 # replica graph (and creates nTDSConnection objects) for the
2369 # config NC as above, except that only NC replicas that "are present"
2370 # on GC servers are added to R.
2371 for connect in mydsa.connect_table.values():
2372 if connect.to_be_added:
2373 DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2375 # Do it again, with gc_only True
2376 for partdn, part in self.part_table.items():
2377 if part.is_config():
2378 self.construct_intrasite_graph(mysite, mydsa, part, True,
2381 # The DC repeats the NC replica graph computation and nTDSConnection
2382 # creation for each of the NC replica graphs, this time assuming
2383 # that no DC has failed. It does so by re-executing the steps as
2384 # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2385 # set in the options attribute of the site settings object for
2386 # the local DC's site. (ie. we set "detec_stale" flag to False)
2387 for connect in mydsa.connect_table.values():
2388 if connect.to_be_added:
2389 DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2391 # Loop thru all the partitions.
2392 for partdn, part in self.part_table.items():
2393 self.construct_intrasite_graph(mysite, mydsa, part, False,
2394 False) # don't detect stale
2396 # If the DC is a GC server, the KCC constructs an additional NC
2397 # replica graph (and creates nTDSConnection objects) for the
2398 # config NC as above, except that only NC replicas that "are present"
2399 # on GC servers are added to R.
2400 for connect in mydsa.connect_table.values():
2401 if connect.to_be_added:
2402 DEBUG_RED("TO BE ADDED:\n%s" % connect)
2404 for partdn, part in self.part_table.items():
2405 if part.is_config():
2406 self.construct_intrasite_graph(mysite, mydsa, part, True,
2407 False) # don't detect stale
2410 # Display any to be added or modified repsFrom
2411 for connect in mydsa.connect_table.values():
2412 if connect.to_be_deleted:
2413 logger.info("TO BE DELETED:\n%s" % connect)
2414 if connect.to_be_modified:
2415 logger.info("TO BE MODIFIED:\n%s" % connect)
2416 if connect.to_be_added:
2417 DEBUG_GREEN("TO BE ADDED:\n%s" % connect)
2419 mydsa.commit_connections(self.samdb, ro=True)
2421 # Commit any newly created connections to the samdb
2422 mydsa.commit_connections(self.samdb)
2424 def list_dsas(self):
2428 self.load_all_sites()
2429 self.load_all_partitions()
2430 self.load_all_transports()
2431 self.load_all_sitelinks()
2433 for site in self.site_table.values():
2434 dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2435 for dsa in site.dsa_table.values()])
2438 def load_samdb(self, dburl, lp, creds):
2439 self.samdb = SamDB(url=dburl,
2440 session_info=system_session(),
2441 credentials=creds, lp=lp)
2443 def plot_all_connections(self, basename, verify_properties=()):
2444 verify = verify_properties and opts.verify
2445 plot = opts.dot_files
2446 if not (verify or plot):
2454 for dsa in self.dsa_by_dnstr.values():
2455 dot_vertices.append(dsa.dsa_dnstr)
2457 vertex_colours.append('#cc0000')
2459 vertex_colours.append('#0000cc')
2460 for con in dsa.connect_table.values():
2461 if con.is_rodc_topology():
2462 edge_colours.append('red')
2464 edge_colours.append('blue')
2465 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2467 verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2468 label=self.my_dsa_dnstr, properties=verify_properties,
2469 debug=DEBUG, verify=verify, dot_files=plot,
2470 directed=True, edge_colors=edge_colours,
2471 vertex_colors=vertex_colours)
2473 def run(self, dburl, lp, creds, forced_local_dsa=None,
2474 forget_local_links=False, forget_intersite_links=False):
2475 """Method to perform a complete run of the KCC and
2476 produce an updated topology for subsequent NC replica
2477 syncronization between domain controllers
2479 # We may already have a samdb setup if we are
2480 # currently importing an ldif for a test run
2481 if self.samdb is None:
2483 self.load_samdb(dburl, lp, creds)
2484 except ldb.LdbError, (num, msg):
2485 logger.error("Unable to open sam database %s : %s" %
2489 if forced_local_dsa:
2490 self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" %
2498 self.load_all_sites()
2499 self.load_all_partitions()
2500 self.load_all_transports()
2501 self.load_all_sitelinks()
2503 if opts.verify or opts.dot_files:
2505 for site in self.site_table.values():
2506 guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2508 in site.dsa_table.items())
2510 self.plot_all_connections('dsa_initial')
2513 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2514 for dnstr, c_rep in current_reps.items():
2515 DEBUG("c_rep %s" % c_rep)
2516 dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2518 verify_and_dot('dsa_repsFrom_initial', dot_edges,
2519 directed=True, label=self.my_dsa_dnstr,
2520 properties=(), debug=DEBUG, verify=opts.verify,
2521 dot_files=opts.dot_files)
2524 for site in self.site_table.values():
2525 for dsa in site.dsa_table.values():
2526 current_reps, needed_reps = dsa.get_rep_tables()
2527 for dn_str, rep in current_reps.items():
2528 for reps_from in rep.rep_repsFrom:
2529 DEBUG("rep %s" % rep)
2530 dsa_guid = str(reps_from.source_dsa_obj_guid)
2531 dsa_dn = guid_to_dnstr[dsa_guid]
2532 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2534 verify_and_dot('dsa_repsFrom_initial_all', dot_edges,
2535 directed=True, label=self.my_dsa_dnstr,
2536 properties=(), debug=DEBUG, verify=opts.verify,
2537 dot_files=opts.dot_files)
2540 for link in self.sitelink_table.values():
2541 for a, b in itertools.combinations(link.site_list, 2):
2542 dot_edges.append((str(a), str(b)))
2543 properties = ('connected',)
2544 verify_and_dot('dsa_sitelink_initial', dot_edges,
2546 label=self.my_dsa_dnstr, properties=properties,
2547 debug=DEBUG, verify=opts.verify,
2548 dot_files=opts.dot_files)
2550 if forget_local_links:
2551 for dsa in self.my_site.dsa_table.values():
2552 dsa.connect_table = {k: v for k, v in
2553 dsa.connect_table.items()
2554 if v.is_rodc_topology()}
2555 self.plot_all_connections('dsa_forgotten_local')
2557 if forget_intersite_links:
2558 for site in self.site_table.values():
2559 for dsa in site.dsa_table.values():
2560 dsa.connect_table = {k: v for k, v in
2561 dsa.connect_table.items()
2562 if site is self.my_site and
2563 v.is_rodc_topology()}
2565 self.plot_all_connections('dsa_forgotten_all')
2566 # These are the published steps (in order) for the
2567 # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2570 self.refresh_failed_links_connections()
2576 all_connected = self.intersite()
2579 self.remove_unneeded_ntdsconn(all_connected)
2582 self.translate_ntdsconn()
2585 self.remove_unneeded_failed_links_connections()
2588 self.update_rodc_connection()
2590 if opts.verify or opts.dot_files:
2591 self.plot_all_connections('dsa_final',
2592 ('connected', 'forest_of_rings'))
2594 DEBUG_MAGENTA("there are %d dsa guids" % len(guid_to_dnstr))
2598 my_dnstr = self.my_dsa.dsa_dnstr
2599 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2600 for dnstr, n_rep in needed_reps.items():
2601 for reps_from in n_rep.rep_repsFrom:
2602 guid_str = str(reps_from.source_dsa_obj_guid)
2603 dot_edges.append((my_dnstr, guid_to_dnstr[guid_str]))
2604 edge_colors.append('#' + str(n_rep.nc_guid)[:6])
2606 verify_and_dot('dsa_repsFrom_final', dot_edges, directed=True,
2607 label=self.my_dsa_dnstr,
2608 properties=(), debug=DEBUG, verify=opts.verify,
2609 dot_files=opts.dot_files,
2610 edge_colors=edge_colors)
2614 for site in self.site_table.values():
2615 for dsa in site.dsa_table.values():
2616 current_reps, needed_reps = dsa.get_rep_tables()
2617 for n_rep in needed_reps.values():
2618 for reps_from in n_rep.rep_repsFrom:
2619 dsa_guid = str(reps_from.source_dsa_obj_guid)
2620 dsa_dn = guid_to_dnstr[dsa_guid]
2621 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2623 verify_and_dot('dsa_repsFrom_final_all', dot_edges,
2624 directed=True, label=self.my_dsa_dnstr,
2625 properties=(), debug=DEBUG, verify=opts.verify,
2626 dot_files=opts.dot_files)
2633 def import_ldif(self, dburl, lp, creds, ldif_file):
2634 """Import all objects and attributes that are relevent
2635 to the KCC algorithms from a previously exported LDIF file.
2637 The point of this function is to allow a programmer/debugger to
2638 import an LDIF file with non-security relevent information that
2639 was previously extracted from a DC database. The LDIF file is used
2640 to create a temporary abbreviated database. The KCC algorithm can
2641 then run against this abbreviated database for debug or test
2642 verification that the topology generated is computationally the
2643 same between different OSes and algorithms.
2645 :param dburl: path to the temporary abbreviated db to create
2646 :param ldif_file: path to the ldif file to import
2649 self.samdb = ldif_utils.ldif_to_samdb(dburl, lp, ldif_file,
2650 opts.forced_local_dsa)
2651 except ldif_utils.LdifError, e:
2656 def export_ldif(self, dburl, lp, creds, ldif_file):
2657 """Routine to extract all objects and attributes that are relevent
2658 to the KCC algorithms from a DC database.
2660 The point of this function is to allow a programmer/debugger to
2661 extract an LDIF file with non-security relevent information from
2662 a DC database. The LDIF file can then be used to "import" via
2663 the import_ldif() function this file into a temporary abbreviated
2664 database. The KCC algorithm can then run against this abbreviated
2665 database for debug or test verification that the topology generated
2666 is computationally the same between different OSes and algorithms.
2668 :param dburl: LDAP database URL to extract info from
2669 :param ldif_file: output LDIF file name to create
2672 ldif_utils.samdb_to_ldif_file(self.samdb, dburl, lp, creds,
2674 except ldif_utils.LdifError, e:
2679 ##################################################
2681 ##################################################
2684 def get_spanning_tree_edges(graph, my_site, label=None):
2685 # Phase 1: Run Dijkstra's to get a list of internal edges, which are
2686 # just the shortest-paths connecting colored vertices
2688 internal_edges = set()
2690 for e_set in graph.edge_set:
2692 for v in graph.vertices:
2695 # All con_type in an edge set is the same
2696 for e in e_set.edges:
2697 edgeType = e.con_type
2698 for v in e.vertices:
2701 if opts.verify or opts.dot_files:
2702 graph_edges = [(a.site.site_dnstr, b.site.site_dnstr)
2705 *(itertools.combinations(edge.vertices, 2)
2706 for edge in e_set.edges))]
2707 graph_nodes = [v.site.site_dnstr for v in graph.vertices]
2709 if opts.dot_files and opts.debug:
2710 write_dot_file('edgeset_%s' % (edgeType,), graph_edges,
2711 vertices=graph_nodes, label=label)
2714 verify_graph('spanning tree edge set %s' % edgeType,
2715 graph_edges, vertices=graph_nodes,
2716 properties=('complete', 'connected'),
2719 # Run dijkstra's algorithm with just the red vertices as seeds
2720 # Seed from the full replicas
2721 dijkstra(graph, edgeType, False)
2724 process_edge_set(graph, e_set, internal_edges)
2726 # Run dijkstra's algorithm with red and black vertices as the seeds
2727 # Seed from both full and partial replicas
2728 dijkstra(graph, edgeType, True)
2731 process_edge_set(graph, e_set, internal_edges)
2733 # All vertices have root/component as itself
2734 setup_vertices(graph)
2735 process_edge_set(graph, None, internal_edges)
2737 if opts.verify or opts.dot_files:
2738 graph_edges = [(e.v1.site.site_dnstr, e.v2.site.site_dnstr)
2739 for e in internal_edges]
2740 graph_nodes = [v.site.site_dnstr for v in graph.vertices]
2741 verify_properties = ('multi_edge_forest',)
2742 verify_and_dot('prekruskal', graph_edges, graph_nodes, label=label,
2743 properties=verify_properties, debug=DEBUG,
2745 dot_files=opts.dot_files)
2747 # Phase 2: Run Kruskal's on the internal edges
2748 output_edges, components = kruskal(graph, internal_edges)
2750 # This recalculates the cost for the path connecting the
2751 # closest red vertex. Ignoring types is fine because NO
2752 # suboptimal edge should exist in the graph
2753 dijkstra(graph, "EDGE_TYPE_ALL", False) # TODO rename
2754 # Phase 3: Process the output
2755 for v in graph.vertices:
2759 v.dist_to_red = v.repl_info.cost
2761 if opts.verify or opts.dot_files:
2762 graph_edges = [(e.v1.site.site_dnstr, e.v2.site.site_dnstr)
2763 for e in internal_edges]
2764 graph_nodes = [v.site.site_dnstr for v in graph.vertices]
2765 verify_properties = ('multi_edge_forest',)
2766 verify_and_dot('postkruskal', graph_edges, graph_nodes,
2767 label=label, properties=verify_properties,
2768 debug=DEBUG, verify=opts.verify,
2769 dot_files=opts.dot_files)
2771 # Ensure only one-way connections for partial-replicas,
2772 # and make sure they point the right way.
2774 for edge in output_edges:
2775 # We know these edges only have two endpoints because we made
2777 v, w = edge.vertices
2778 if v.site is my_site or w.site is my_site:
2779 if (((v.is_black() or w.is_black()) and
2780 v.dist_to_red != MAX_DWORD)):
2781 edge.directed = True
2783 if w.dist_to_red < v.dist_to_red:
2784 edge.vertices[:] = w, v
2785 edge_list.append(edge)
2787 if opts.verify or opts.dot_files:
2788 graph_edges = [[x.site.site_dnstr for x in e.vertices]
2790 #add the reverse edge if not directed.
2791 graph_edges.extend([x.site.site_dnstr
2792 for x in reversed(e.vertices)]
2793 for e in edge_list if not e.directed)
2794 graph_nodes = [x.site.site_dnstr for x in graph.vertices]
2795 verify_properties = ()
2796 verify_and_dot('post-one-way-partial', graph_edges, graph_nodes,
2797 label=label, properties=verify_properties,
2798 debug=DEBUG, verify=opts.verify,
2800 dot_files=opts.dot_files)
2802 # count the components
2803 return edge_list, components
2806 def sort_replica_by_dsa_guid(rep1, rep2):
2807 """Helper to sort NCReplicas by their DSA guids
2809 The guids need to be sorted in their NDR form.
2811 :param rep1: An NC replica
2812 :param rep2: Another replica
2813 :return: -1, 0, or 1, indicating sort order.
2815 return cmp(ndr_pack(rep1.rep_dsa_guid), ndr_pack(rep2.rep_dsa_guid))
2818 def sort_dsa_by_gc_and_guid(dsa1, dsa2):
2819 """Helper to sort DSAs by guid global catalog status
2821 GC DSAs come before non-GC DSAs, other than that, the guids are
2824 :param dsa1: A DSA object
2825 :param dsa2: Another DSA
2826 :return: -1, 0, or 1, indicating sort order.
2828 if dsa1.is_gc() and not dsa2.is_gc():
2830 if not dsa1.is_gc() and dsa2.is_gc():
2832 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
2835 def is_smtp_replication_available():
2836 """Can the KCC use SMTP replication?
2838 Currently always returns false because Samba doesn't implement
2839 SMTP transfer for NC changes between DCs.
2841 :return: Boolean (always False)
2846 def create_edge(con_type, site_link, guid_to_vertex):
2848 e.site_link = site_link
2850 for site_guid in site_link.site_list:
2851 if str(site_guid) in guid_to_vertex:
2852 e.vertices.extend(guid_to_vertex.get(str(site_guid)))
2853 e.repl_info.cost = site_link.cost
2854 e.repl_info.options = site_link.options
2855 e.repl_info.interval = site_link.interval
2856 e.repl_info.schedule = convert_schedule_to_repltimes(site_link.schedule)
2857 e.con_type = con_type
2862 def create_auto_edge_set(graph, transport):
2863 e_set = MultiEdgeSet()
2864 # use a NULL guid, not associated with a SiteLinkBridge object
2865 e_set.guid = misc.GUID()
2866 for site_link in graph.edges:
2867 if site_link.con_type == transport:
2868 e_set.edges.append(site_link)
2873 def create_edge_set(graph, transport, site_link_bridge):
2874 # TODO not implemented - need to store all site link bridges
2875 e_set = MultiEdgeSet()
2876 # e_set.guid = site_link_bridge
2880 def setup_vertices(graph):
2881 for v in graph.vertices:
2883 v.repl_info.cost = MAX_DWORD
2885 v.component_id = None
2887 v.repl_info.cost = 0
2891 v.repl_info.interval = 0
2892 v.repl_info.options = 0xFFFFFFFF
2893 v.repl_info.schedule = None # TODO highly suspicious
2897 def dijkstra(graph, edge_type, include_black):
2899 setup_dijkstra(graph, edge_type, include_black, queue)
2900 while len(queue) > 0:
2901 cost, guid, vertex = heapq.heappop(queue)
2902 for edge in vertex.edges:
2903 for v in edge.vertices:
2905 # add new path from vertex to v
2906 try_new_path(graph, queue, vertex, edge, v)
2909 def setup_dijkstra(graph, edge_type, include_black, queue):
2910 setup_vertices(graph)
2911 for vertex in graph.vertices:
2912 if vertex.is_white():
2915 if (((vertex.is_black() and not include_black)
2916 or edge_type not in vertex.accept_black
2917 or edge_type not in vertex.accept_red_red)):
2918 vertex.repl_info.cost = MAX_DWORD
2919 vertex.root = None # NULL GUID
2920 vertex.demoted = True # Demoted appears not to be used
2922 heapq.heappush(queue, (vertex.repl_info.cost, vertex.guid, vertex))
2925 def try_new_path(graph, queue, vfrom, edge, vto):
2927 # What this function checks is that there is a valid time frame for
2928 # which replication can actually occur, despite being adequately
2930 intersect = combine_repl_info(vfrom.repl_info, edge.repl_info, newRI)
2932 # If the new path costs more than the current, then ignore the edge
2933 if newRI.cost > vto.repl_info.cost:
2936 if newRI.cost < vto.repl_info.cost and not intersect:
2939 new_duration = total_schedule(newRI.schedule)
2940 old_duration = total_schedule(vto.repl_info.schedule)
2942 # Cheaper or longer schedule
2943 if newRI.cost < vto.repl_info.cost or new_duration > old_duration:
2944 vto.root = vfrom.root
2945 vto.component_id = vfrom.component_id
2946 vto.repl_info = newRI
2947 heapq.heappush(queue, (vto.repl_info.cost, vto.guid, vto))
2950 def check_demote_vertex(vertex, edge_type):
2951 if vertex.is_white():
2954 # Accepts neither red-red nor black edges, demote
2955 if ((edge_type not in vertex.accept_black and
2956 edge_type not in vertex.accept_red_red)):
2957 vertex.repl_info.cost = MAX_DWORD
2959 vertex.demoted = True # Demoted appears not to be used
2962 def undemote_vertex(vertex):
2963 if vertex.is_white():
2966 vertex.repl_info.cost = 0
2967 vertex.root = vertex
2968 vertex.demoted = False
2971 def process_edge_set(graph, e_set, internal_edges):
2973 for edge in graph.edges:
2974 for vertex in edge.vertices:
2975 check_demote_vertex(vertex, edge.con_type)
2976 process_edge(graph, edge, internal_edges)
2977 for vertex in edge.vertices:
2978 undemote_vertex(vertex)
2980 for edge in e_set.edges:
2981 process_edge(graph, edge, internal_edges)
2984 def process_edge(graph, examine, internal_edges):
2985 # Find the set of all vertices touches the edge to examine
2987 for v in examine.vertices:
2988 # Append a 4-tuple of color, repl cost, guid and vertex
2989 vertices.append((v.color, v.repl_info.cost, v.ndrpacked_guid, v))
2990 # Sort by color, lower
2991 DEBUG("vertices is %s" % vertices)
2994 color, cost, guid, bestv = vertices[0]
2995 # Add to internal edges an edge from every colored vertex to bestV
2996 for v in examine.vertices:
2997 if v.component_id is None or v.root is None:
3000 # Only add edge if valid inter-tree edge - needs a root and
3001 # different components
3002 if ((bestv.component_id is not None and
3003 bestv.root is not None and
3004 v.component_id is not None and
3005 v.root is not None and
3006 bestv.component_id != v.component_id)):
3007 add_int_edge(graph, internal_edges, examine, bestv, v)
3010 # Add internal edge, endpoints are roots of the vertices to pass in
3011 # and are always colored
3012 def add_int_edge(graph, internal_edges, examine, v1, v2):
3017 if root1.is_red() and root2.is_red():
3021 if ((examine.con_type not in root1.accept_red_red
3022 or examine.con_type not in root2.accept_red_red)):
3024 elif (examine.con_type not in root1.accept_black
3025 or examine.con_type not in root2.accept_black):
3031 # Create the transitive replInfo for the two trees and this edge
3032 if not combine_repl_info(v1.repl_info, v2.repl_info, ri):
3034 # ri is now initialized
3035 if not combine_repl_info(ri, examine.repl_info, ri2):
3038 newIntEdge = InternalEdge(root1, root2, red_red, ri2, examine.con_type,
3040 # Order by vertex guid
3041 #XXX guid comparison using ndr_pack
3042 if newIntEdge.v1.ndrpacked_guid > newIntEdge.v2.ndrpacked_guid:
3043 newIntEdge.v1 = root2
3044 newIntEdge.v2 = root1
3046 internal_edges.add(newIntEdge)
3049 def kruskal(graph, edges):
3050 for v in graph.vertices:
3053 components = set([x for x in graph.vertices if not x.is_white()])
3056 # Sorted based on internal comparison function of internal edge
3059 #XXX expected_num_tree_edges is never used
3060 expected_num_tree_edges = 0 # TODO this value makes little sense
3065 while index < len(edges): # TODO and num_components > 1
3067 parent1 = find_component(e.v1)
3068 parent2 = find_component(e.v2)
3069 if parent1 is not parent2:
3071 add_out_edge(graph, output_edges, e)
3072 parent1.component_id = parent2
3073 components.discard(parent1)
3077 return output_edges, len(components)
3080 def find_component(vertex):
3081 if vertex.component_id is vertex:
3085 while current.component_id is not current:
3086 current = current.component_id
3090 while current.component_id is not root:
3091 n = current.component_id
3092 current.component_id = root
3098 def add_out_edge(graph, output_edges, e):
3102 # This multi-edge is a 'real' edge with no GUID
3105 ee.site_link = e.site_link
3106 ee.vertices.append(v1)
3107 ee.vertices.append(v2)
3108 ee.con_type = e.e_type
3109 ee.repl_info = e.repl_info
3110 output_edges.append(ee)
3116 def test_all_reps_from(lp, creds, rng_seed=None):
3118 kcc.load_samdb(opts.dburl, lp, creds)
3119 dsas = kcc.list_dsas()
3124 for site in kcc.site_table.values():
3125 guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
3126 for dnstr, dsa in site.dsa_table.items())
3135 random.seed(rng_seed)
3137 kcc.run(opts.dburl, lp, creds, forced_local_dsa=dsa_dn,
3138 forget_local_links=opts.forget_local_links,
3139 forget_intersite_links=opts.forget_intersite_links)
3141 current, needed = kcc.my_dsa.get_rep_tables()
3143 for dsa in kcc.my_site.dsa_table.values():
3144 if dsa is kcc.my_dsa:
3146 kcc.translate_ntdsconn(dsa)
3147 c, n = dsa.get_rep_tables()
3151 for name, rep_table, rep_parts in (
3152 ('needed', needed, needed_parts),
3153 ('current', current, current_parts)):
3154 for part, nc_rep in rep_table.items():
3155 edges = rep_parts.setdefault(part, [])
3156 for reps_from in nc_rep.rep_repsFrom:
3157 source = guid_to_dnstr[str(reps_from.source_dsa_obj_guid)]
3158 dest = guid_to_dnstr[str(nc_rep.rep_dsa_guid)]
3159 edges.append((source, dest))
3161 for site in kcc.site_table.values():
3162 for dsa in site.dsa_table.values():
3164 vertex_colours.append('#cc0000')
3166 vertex_colours.append('#0000cc')
3167 dot_vertices.append(dsa.dsa_dnstr)
3168 if dsa.connect_table:
3169 DEBUG_FN("DSA %s %s connections:\n%s" %
3170 (dsa.dsa_dnstr, len(dsa.connect_table),
3171 [x.from_dnstr for x in
3172 dsa.connect_table.values()]))
3173 for con in dsa.connect_table.values():
3174 if con.is_rodc_topology():
3175 colours.append('red')
3177 colours.append('blue')
3178 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
3180 verify_and_dot('all-dsa-connections', dot_edges, vertices=dot_vertices,
3181 label="all dsa NTDSConnections", properties=(),
3182 debug=DEBUG, verify=opts.verify, dot_files=opts.dot_files,
3183 directed=True, edge_colors=colours,
3184 vertex_colors=vertex_colours)
3186 for name, rep_parts in (('needed', needed_parts),
3187 ('current', current_parts)):
3188 for part, edges in rep_parts.items():
3189 verify_and_dot('all-repsFrom_%s__%s' % (name, part), edges,
3190 directed=True, label=part,
3191 properties=(), debug=DEBUG, verify=opts.verify,
3192 dot_files=opts.dot_files)
3195 logger = logging.getLogger("samba_kcc")
3196 logger.addHandler(logging.StreamHandler(sys.stdout))
3197 DEBUG = logger.debug
3200 def _color_debug(*args, **kwargs):
3201 DEBUG('%s%s%s' % (kwargs['color'], args[0], C_NORMAL), *args[1:])
3203 _globals = globals()
3204 for _color in ('DARK_RED', 'RED', 'DARK_GREEN', 'GREEN', 'YELLOW',
3205 'DARK_YELLOW', 'DARK_BLUE', 'BLUE', 'PURPLE', 'MAGENTA',
3206 'DARK_CYAN', 'CYAN', 'GREY', 'WHITE', 'REV_RED'):
3207 _globals['DEBUG_' + _color] = partial(_color_debug, color=_globals[_color])
3210 def DEBUG_FN(msg=''):
3212 filename, lineno, function, text = traceback.extract_stack(None, 2)[0]
3213 DEBUG("%s%s:%s%s %s%s()%s '%s'" % (CYAN, filename, BLUE, lineno,
3214 CYAN, function, C_NORMAL, msg))
3217 ##################################################
3218 # samba_kcc entry point
3219 ##################################################
3221 parser = optparse.OptionParser("samba_kcc [options]")
3222 sambaopts = options.SambaOptions(parser)
3223 credopts = options.CredentialsOptions(parser)
3225 parser.add_option_group(sambaopts)
3226 parser.add_option_group(credopts)
3227 parser.add_option_group(options.VersionOptions(parser))
3229 parser.add_option("--readonly", default=False,
3230 help="compute topology but do not update database",
3231 action="store_true")
3233 parser.add_option("--debug",
3234 help="debug output",
3235 action="store_true")
3237 parser.add_option("--verify",
3238 help="verify that assorted invariants are kept",
3239 action="store_true")
3241 parser.add_option("--list-verify-tests",
3242 help=("list what verification actions are available "
3243 "and do nothing else"),
3244 action="store_true")
3246 parser.add_option("--no-dot-files", dest='dot_files',
3247 help="Don't write dot graph files in /tmp",
3248 default=True, action="store_false")
3250 parser.add_option("--seed",
3251 help="random number seed",
3254 parser.add_option("--importldif",
3255 help="import topology ldif file",
3256 type=str, metavar="<file>")
3258 parser.add_option("--exportldif",
3259 help="export topology ldif file",
3260 type=str, metavar="<file>")
3262 parser.add_option("-H", "--URL",
3263 help="LDB URL for database or target server",
3264 type=str, metavar="<URL>", dest="dburl")
3266 parser.add_option("--tmpdb",
3267 help="schemaless database file to create for ldif import",
3268 type=str, metavar="<file>")
3270 parser.add_option("--now",
3271 help=("assume current time is this ('YYYYmmddHHMMSS[tz]',"
3272 " default: system time)"),
3273 type=str, metavar="<date>")
3275 parser.add_option("--forced-local-dsa",
3276 help="run calculations assuming the DSA is this DN",
3277 type=str, metavar="<DSA>")
3279 parser.add_option("--attempt-live-connections", default=False,
3280 help="Attempt to connect to other DSAs to test links",
3281 action="store_true")
3283 parser.add_option("--list-valid-dsas", default=False,
3284 help=("Print a list of DSA dnstrs that could be"
3285 " used in --forced-local-dsa"),
3286 action="store_true")
3288 parser.add_option("--test-all-reps-from", default=False,
3289 help="Create and verify a graph of reps-from for every DSA",
3290 action="store_true")
3292 parser.add_option("--forget-local-links", default=False,
3293 help="pretend not to know the existing local topology",
3294 action="store_true")
3296 parser.add_option("--forget-intersite-links", default=False,
3297 help="pretend not to know the existing intersite topology",
3298 action="store_true")
3301 opts, args = parser.parse_args()
3304 if opts.list_verify_tests:
3309 logger.setLevel(logging.DEBUG)
3311 logger.setLevel(logging.INFO)
3313 logger.setLevel(logging.WARNING)
3315 # initialize seed from optional input parameter
3317 random.seed(opts.seed)
3319 random.seed(0xACE5CA11)
3322 for timeformat in ("%Y%m%d%H%M%S%Z", "%Y%m%d%H%M%S"):
3324 now_tuple = time.strptime(opts.now, timeformat)
3329 # else happens if break doesn't --> no match
3330 print >> sys.stderr, "could not parse time '%s'" % opts.now
3333 unix_now = int(time.mktime(now_tuple))
3335 unix_now = int(time.time())
3337 nt_now = unix2nttime(unix_now)
3339 lp = sambaopts.get_loadparm()
3340 creds = credopts.get_credentials(lp, fallback_machine=True)
3342 if opts.dburl is None:
3343 opts.dburl = lp.samdb_url()
3345 if opts.test_all_reps_from:
3346 opts.readonly = True
3347 rng_seed = opts.seed or 0xACE5CA11
3348 test_all_reps_from(lp, creds, rng_seed=rng_seed)
3351 # Instantiate Knowledge Consistency Checker and perform run
3355 rc = kcc.export_ldif(opts.dburl, lp, creds, opts.exportldif)
3359 if opts.tmpdb is None or opts.tmpdb.startswith('ldap'):
3360 logger.error("Specify a target temp database file with --tmpdb option")
3363 rc = kcc.import_ldif(opts.tmpdb, lp, creds, opts.importldif)
3367 if opts.list_valid_dsas:
3368 kcc.load_samdb(opts.dburl, lp, creds)
3369 print '\n'.join(kcc.list_dsas())
3373 rc = kcc.run(opts.dburl, lp, creds, opts.forced_local_dsa,
3374 opts.forget_local_links, opts.forget_intersite_links)
3377 except GraphError, e: