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
57 from samba.auth import system_session
58 from samba.samdb import SamDB
59 from samba.dcerpc import drsuapi
60 from samba.kcc_utils import *
61 from samba.graph_utils import *
62 from samba import ldif_utils
65 """The Knowledge Consistency Checker class.
67 A container for objects and methods allowing a run of the KCC. Produces a
68 set of connections in the samdb for which the Distributed Replication
69 Service can then utilize to replicate naming contexts
72 """Initializes the partitions class which can hold
73 our local DCs partitions or all the partitions in
76 self.part_table = {} # partition objects
78 self.transport_table = {}
79 self.sitelink_table = {}
80 self.dsa_by_dnstr = {}
83 self.get_dsa_by_guidstr = self.dsa_by_guid.get
84 self.get_dsa = self.dsa_by_dnstr.get
86 # TODO: These should be backed by a 'permanent' store so that when
87 # calling DRSGetReplInfo with DS_REPL_INFO_KCC_DSA_CONNECT_FAILURES,
88 # the failure information can be returned
89 self.kcc_failed_links = {}
90 self.kcc_failed_connections = set()
92 # Used in inter-site topology computation. A list
93 # of connections (by NTDSConnection object) that are
94 # to be kept when pruning un-needed NTDS Connections
95 self.kept_connections = set()
97 self.my_dsa_dnstr = None # My dsa DN
98 self.my_dsa = None # My dsa object
100 self.my_site_dnstr = None
105 def load_all_transports(self):
106 """Loads the inter-site transport objects for Sites
108 ::returns: Raises an Exception on error
111 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
112 self.samdb.get_config_basedn(),
113 scope=ldb.SCOPE_SUBTREE,
114 expression="(objectClass=interSiteTransport)")
115 except ldb.LdbError, (enum, estr):
116 raise Exception("Unable to find inter-site transports - (%s)" %
122 transport = Transport(dnstr)
124 transport.load_transport(self.samdb)
127 if str(transport.guid) in self.transport_table:
130 # Assign this transport to table
132 self.transport_table[str(transport.guid)] = transport
134 def load_all_sitelinks(self):
135 """Loads the inter-site siteLink objects
137 ::returns: Raises an Exception on error
140 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
141 self.samdb.get_config_basedn(),
142 scope=ldb.SCOPE_SUBTREE,
143 expression="(objectClass=siteLink)")
144 except ldb.LdbError, (enum, estr):
145 raise Exception("Unable to find inter-site siteLinks - (%s)" % estr)
151 if dnstr in self.sitelink_table:
154 sitelink = SiteLink(dnstr)
156 sitelink.load_sitelink(self.samdb)
158 # Assign this siteLink to table
160 self.sitelink_table[dnstr] = sitelink
162 def load_site(self, dn_str):
163 """Helper for load_my_site and load_all_sites. It puts all the site's
164 DSAs into the KCC indices.
166 site = Site(dn_str, unix_now)
167 site.load_site(self.samdb)
169 # I am not sure why, but we avoid replacing the site with an
171 guid = str(site.site_guid)
172 if guid not in self.site_table:
173 self.site_table[guid] = site
175 self.dsa_by_dnstr.update(site.dsa_table)
176 self.dsa_by_guid.update((str(x.dsa_guid), x) for x in site.dsa_table.values())
180 def load_my_site(self):
181 """Loads the Site class for the local DSA
183 ::returns: Raises an Exception on error
185 self.my_site_dnstr = "CN=%s,CN=Sites,%s" % (
186 self.samdb.server_site_name(),
187 self.samdb.get_config_basedn())
189 self.my_site = self.load_site(self.my_site_dnstr)
191 def load_all_sites(self):
192 """Discover all sites and instantiate and load each
195 ::returns: Raises an Exception on error
198 res = self.samdb.search("CN=Sites,%s" %
199 self.samdb.get_config_basedn(),
200 scope=ldb.SCOPE_SUBTREE,
201 expression="(objectClass=site)")
202 except ldb.LdbError, (enum, estr):
203 raise Exception("Unable to find sites - (%s)" % estr)
206 sitestr = str(msg.dn)
207 self.load_site(sitestr)
209 def load_my_dsa(self):
210 """Discover my nTDSDSA dn thru the rootDSE entry
212 ::returns: Raises an Exception on error.
214 dn = ldb.Dn(self.samdb, "<GUID=%s>" % self.samdb.get_ntds_GUID())
216 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
217 attrs=["objectGUID"])
218 except ldb.LdbError, (enum, estr):
219 DEBUG("Search for %s failed: %s. This typically happens in"
220 " --importldif mode due to lack of module support",
223 # We work around the failure above by looking at the
224 # dsServiceName that was put in the fake rootdse by
225 # the --exportldif, rather than the
226 # samdb.get_ntds_GUID(). The disadvantage is that this
227 # mode requires we modify the @ROOTDSE dnq to support
229 service_name_res = self.samdb.search(base="", scope=ldb.SCOPE_BASE,
230 attrs=["dsServiceName"])
231 dn = ldb.Dn(self.samdb, service_name_res[0]["dsServiceName"][0])
233 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
234 attrs=["objectGUID"])
235 except ldb.LdbError, (enum, estr):
236 raise Exception("Unable to find my nTDSDSA - (%s)" % estr)
239 raise Exception("Unable to find my nTDSDSA at %s" % dn.extended_str())
241 if misc.GUID(res[0]["objectGUID"][0]) != misc.GUID(self.samdb.get_ntds_GUID()):
242 raise Exception("Did not find the GUID we expected, perhaps due to --importldif")
244 self.my_dsa_dnstr = str(res[0].dn)
246 self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
248 def load_all_partitions(self):
249 """Discover all NCs thru the Partitions dn and
250 instantiate and load the NCs.
252 Each NC is inserted into the part_table by partition
253 dn string (not the nCName dn string)
255 ::returns: Raises an Exception on error
258 res = self.samdb.search("CN=Partitions,%s" %
259 self.samdb.get_config_basedn(),
260 scope=ldb.SCOPE_SUBTREE,
261 expression="(objectClass=crossRef)")
262 except ldb.LdbError, (enum, estr):
263 raise Exception("Unable to find partitions - (%s)" % estr)
266 partstr = str(msg.dn)
269 if partstr in self.part_table:
272 part = Partition(partstr)
274 part.load_partition(self.samdb)
275 self.part_table[partstr] = part
277 def should_be_present_test(self):
278 """Enumerate all loaded partitions and DSAs in local
279 site and test if NC should be present as replica
281 for partdn, part in self.part_table.items():
282 for dsadn, dsa in self.my_site.dsa_table.items():
283 needed, ro, partial = part.should_be_present(dsa)
284 logger.info("dsadn:%s\nncdn:%s\nneeded=%s:ro=%s:partial=%s\n" %
285 (dsadn, part.nc_dnstr, needed, ro, partial))
287 def refresh_failed_links_connections(self):
288 """Based on MS-ADTS 6.2.2.1"""
290 # Instead of NULL link with failure_count = 0, the tuple is simply removed
292 # LINKS: Refresh failed links
293 self.kcc_failed_links = {}
294 current, needed = self.my_dsa.get_rep_tables()
295 for replica in current.values():
296 # For every possible connection to replicate
297 for reps_from in replica.rep_repsFrom:
298 failure_count = reps_from.consecutive_sync_failures
299 if failure_count <= 0:
302 dsa_guid = str(reps_from.source_dsa_obj_guid)
303 time_first_failure = reps_from.last_success
304 last_result = reps_from.last_attempt
305 dns_name = reps_from.dns_name1
307 f = self.kcc_failed_links.get(dsa_guid)
309 f = KCCFailedObject(dsa_guid, failure_count,
310 time_first_failure, last_result,
312 self.kcc_failed_links[dsa_guid] = f
313 #elif f.failure_count == 0:
314 # f.failure_count = failure_count
315 # f.time_first_failure = time_first_failure
316 # f.last_result = last_result
318 f.failure_count = max(f.failure_count, failure_count)
319 f.time_first_failure = min(f.time_first_failure, time_first_failure)
320 f.last_result = last_result
322 # CONNECTIONS: Refresh failed connections
323 restore_connections = set()
324 if opts.attempt_live_connections:
325 DEBUG("refresh_failed_links: checking if links are still down")
326 for connection in self.kcc_failed_connections:
328 drs_utils.drsuapi_connect(connection.dns_name, lp, creds)
329 # Failed connection is no longer failing
330 restore_connections.add(connection)
331 except drs_utils.drsException:
332 # Failed connection still failing
333 connection.failure_count += 1
335 DEBUG("refresh_failed_links: not checking live links because we weren't\n"
336 "asked to --attempt-live-connections")
338 # Remove the restored connections from the failed connections
339 self.kcc_failed_connections.difference_update(restore_connections)
341 def is_stale_link_connection(self, target_dsa):
342 """Returns False if no tuple z exists in the kCCFailedLinks or
343 kCCFailedConnections variables such that z.UUIDDsa is the
344 objectGUID of the target dsa, z.FailureCount > 0, and
345 the current time - z.TimeFirstFailure > 2 hours.
347 # Returns True if tuple z exists...
348 failed_link = self.kcc_failed_links.get(str(target_dsa.dsa_guid))
350 # failure_count should be > 0, but check anyways
351 if failed_link.failure_count > 0:
352 unix_first_time_failure = nttime2unix(failed_link.time_first_failure)
353 # TODO guard against future
354 if unix_first_time_failure > unix_now:
355 logger.error("The last success time attribute for \
356 repsFrom is in the future!")
358 # Perform calculation in seconds
359 if (unix_now - unix_first_time_failure) > 60 * 60 * 2:
366 # TODO: This should be backed by some form of local database
367 def remove_unneeded_failed_links_connections(self):
368 # Remove all tuples in kcc_failed_links where failure count = 0
369 # In this implementation, this should never happen.
371 # Remove all connections which were not used this run or connections
372 # that became active during this run.
375 def remove_unneeded_ntdsconn(self, all_connected):
376 """Removes unneeded NTDS Connections after computation
377 of KCC intra and inter-site topology has finished.
381 # Loop thru connections
382 for cn_conn in mydsa.connect_table.values():
383 if cn_conn.guid is None:
385 cn_conn.guid = misc.GUID(str(uuid.uuid4()))
386 cn_conn.whenCreated = nt_now
388 cn_conn.load_connection(self.samdb)
390 for cn_conn in mydsa.connect_table.values():
392 s_dnstr = cn_conn.get_from_dnstr()
394 cn_conn.to_be_deleted = True
397 # Get the source DSA no matter what site
398 s_dsa = self.get_dsa(s_dnstr)
400 #XXX should an RODC be regarded as same site
401 same_site = s_dnstr in self.my_site.dsa_table
403 # Given an nTDSConnection object cn, if the DC with the
404 # nTDSDSA object dc that is the parent object of cn and
405 # the DC with the nTDSDA object referenced by cn!fromServer
406 # are in the same site, the KCC on dc deletes cn if all of
407 # the following are true:
409 # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
411 # No site settings object s exists for the local DC's site, or
412 # bit NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED is clear in
415 # Another nTDSConnection object cn2 exists such that cn and
416 # cn2 have the same parent object, cn!fromServer = cn2!fromServer,
419 # cn!whenCreated < cn2!whenCreated
421 # cn!whenCreated = cn2!whenCreated and
422 # cn!objectGUID < cn2!objectGUID
424 # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
426 if not cn_conn.is_generated():
429 if self.my_site.is_cleanup_ntdsconn_disabled():
432 # Loop thru connections looking for a duplicate that
433 # fulfills the previous criteria
436 for cn2_conn in mydsa.connect_table.values():
437 if cn2_conn is cn_conn:
440 s2_dnstr = cn2_conn.get_from_dnstr()
442 # If the NTDS Connections has a different
443 # fromServer field then no match
444 if s2_dnstr != s_dnstr:
448 lesser = (cn_conn.whenCreated < cn2_conn.whenCreated or
449 (cn_conn.whenCreated == cn2_conn.whenCreated and
450 ndr_pack(cn_conn.guid) < ndr_pack(cn2_conn.guid)))
455 if lesser and not cn_conn.is_rodc_topology():
456 cn_conn.to_be_deleted = True
458 # Given an nTDSConnection object cn, if the DC with the nTDSDSA
459 # object dc that is the parent object of cn and the DC with
460 # the nTDSDSA object referenced by cn!fromServer are in
461 # different sites, a KCC acting as an ISTG in dc's site
462 # deletes cn if all of the following are true:
464 # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
466 # cn!fromServer references an nTDSDSA object for a DC
467 # in a site other than the local DC's site.
469 # The keepConnections sequence returned by
470 # CreateIntersiteConnections() does not contain
471 # cn!objectGUID, or cn is "superseded by" (see below)
472 # another nTDSConnection cn2 and keepConnections
473 # contains cn2!objectGUID.
475 # The return value of CreateIntersiteConnections()
478 # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in
481 else: # different site
483 if not mydsa.is_istg():
486 if not cn_conn.is_generated():
490 # We are directly using this connection in intersite or
491 # we are using a connection which can supersede this one.
493 # MS-ADTS 6.2.2.4 - Removing Unnecessary Connections does not
494 # appear to be correct.
496 # 1. cn!fromServer and cn!parent appear inconsistent with no cn2
497 # 2. The repsFrom do not imply each other
499 if cn_conn in self.kept_connections: # and not_superceded:
502 # This is the result of create_intersite_connections
503 if not all_connected:
506 if not cn_conn.is_rodc_topology():
507 cn_conn.to_be_deleted = True
510 if mydsa.is_ro() or opts.readonly:
511 for connect in mydsa.connect_table.values():
512 if connect.to_be_deleted:
513 DEBUG_GREEN("TO BE DELETED:\n%s" % connect)
514 if connect.to_be_added:
515 DEBUG_GREEN("TO BE ADDED:\n%s" % connect)
517 # Peform deletion from our tables but perform
518 # no database modification
519 mydsa.commit_connections(self.samdb, ro=True)
521 # Commit any modified connections
522 mydsa.commit_connections(self.samdb)
524 def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
525 """Part of MS-ADTS 6.2.2.5.
527 Update t_repsFrom if necessary to satisfy requirements. Such
528 updates are typically required when the IDL_DRSGetNCChanges
529 server has moved from one site to another--for example, to
530 enable compression when the server is moved from the
531 client's site to another site.
533 :param n_rep: NC replica we need
534 :param t_repsFrom: repsFrom tuple to modify
535 :param s_rep: NC replica at source DSA
536 :param s_dsa: source DSA
537 :param cn_conn: Local DSA NTDSConnection child
539 ::returns: (update) bit field containing which portion of the
540 repsFrom was modified. This bit field is suitable as input
541 to IDL_DRSReplicaModify ulModifyFields element, as it consists
543 drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
544 drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
545 drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
547 s_dnstr = s_dsa.dsa_dnstr
550 same_site = s_dnstr in self.my_site.dsa_table
552 # if schedule doesn't match then update and modify
553 times = convert_schedule_to_repltimes(cn_conn.schedule)
554 if times != t_repsFrom.schedule:
555 t_repsFrom.schedule = times
556 update |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
558 # Bit DRS_PER_SYNC is set in replicaFlags if and only
559 # if nTDSConnection schedule has a value v that specifies
560 # scheduled replication is to be performed at least once
562 if cn_conn.is_schedule_minimum_once_per_week():
564 if (t_repsFrom.replica_flags &
565 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0:
566 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
568 # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
569 # if the source DSA and the local DC's nTDSDSA object are
570 # in the same site or source dsa is the FSMO role owner
571 # of one or more FSMO roles in the NC replica.
572 if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
574 if (t_repsFrom.replica_flags &
575 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0:
576 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
578 # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
579 # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
580 # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
581 # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
582 # t.replicaFlags if and only if s and the local DC's
583 # nTDSDSA object are in different sites.
584 if (cn_conn.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0:
586 if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
588 if (t_repsFrom.replica_flags &
589 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0:
590 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
594 if (t_repsFrom.replica_flags &
595 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0:
596 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
598 # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
599 # and only if s and the local DC's nTDSDSA object are
600 # not in the same site and the
601 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
602 # clear in cn!options
603 if (not same_site and
605 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
607 if (t_repsFrom.replica_flags &
608 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0:
609 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
611 # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
612 # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
613 if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
615 if (t_repsFrom.replica_flags &
616 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0:
617 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
619 # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
620 # set in t.replicaFlags if and only if cn!enabledConnection = false.
621 if not cn_conn.is_enabled():
623 if (t_repsFrom.replica_flags &
624 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0:
625 t_repsFrom.replica_flags |= \
626 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
628 if (t_repsFrom.replica_flags &
629 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0:
630 t_repsFrom.replica_flags |= \
631 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
633 # If s and the local DC's nTDSDSA object are in the same site,
634 # cn!transportType has no value, or the RDN of cn!transportType
637 # Bit DRS_MAIL_REP in t.replicaFlags is clear.
639 # t.uuidTransport = NULL GUID.
641 # t.uuidDsa = The GUID-based DNS name of s.
645 # Bit DRS_MAIL_REP in t.replicaFlags is set.
647 # If x is the object with dsname cn!transportType,
648 # t.uuidTransport = x!objectGUID.
650 # Let a be the attribute identified by
651 # x!transportAddressAttribute. If a is
652 # the dNSHostName attribute, t.uuidDsa = the GUID-based
653 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
655 # It appears that the first statement i.e.
657 # "If s and the local DC's nTDSDSA object are in the same
658 # site, cn!transportType has no value, or the RDN of
659 # cn!transportType is CN=IP:"
661 # could be a slightly tighter statement if it had an "or"
662 # between each condition. I believe this should
665 # IF (same-site) OR (no-value) OR (type-ip)
667 # because IP should be the primary transport mechanism
668 # (even in inter-site) and the absense of the transportType
669 # attribute should always imply IP no matter if its multi-site
671 # NOTE MS-TECH INCORRECT:
673 # All indications point to these statements above being
674 # incorrectly stated:
676 # t.uuidDsa = The GUID-based DNS name of s.
678 # Let a be the attribute identified by
679 # x!transportAddressAttribute. If a is
680 # the dNSHostName attribute, t.uuidDsa = the GUID-based
681 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
683 # because the uuidDSA is a GUID and not a GUID-base DNS
684 # name. Nor can uuidDsa hold (s!parent)!a if not
685 # dNSHostName. What should have been said is:
687 # t.naDsa = The GUID-based DNS name of s
689 # That would also be correct if transportAddressAttribute
690 # were "mailAddress" because (naDsa) can also correctly
691 # hold the SMTP ISM service address.
693 nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
695 # We're not currently supporting SMTP replication
696 # so is_smtp_replication_available() is currently
697 # always returning False
699 cn_conn.transport_dnstr is None or
700 cn_conn.transport_dnstr.find("CN=IP") == 0 or
701 not is_smtp_replication_available()):
703 if (t_repsFrom.replica_flags &
704 drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0:
705 t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
707 t_repsFrom.transport_guid = misc.GUID()
709 # See (NOTE MS-TECH INCORRECT) above
710 if t_repsFrom.version == 0x1:
711 if t_repsFrom.dns_name1 is None or \
712 t_repsFrom.dns_name1 != nastr:
713 t_repsFrom.dns_name1 = nastr
715 if t_repsFrom.dns_name1 is None or \
716 t_repsFrom.dns_name2 is None or \
717 t_repsFrom.dns_name1 != nastr or \
718 t_repsFrom.dns_name2 != nastr:
719 t_repsFrom.dns_name1 = nastr
720 t_repsFrom.dns_name2 = nastr
723 if (t_repsFrom.replica_flags &
724 drsuapi.DRSUAPI_DRS_MAIL_REP) == 0x0:
725 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_MAIL_REP
727 # We have a transport type but its not an
728 # object in the database
729 if cn_conn.transport_guid not in self.transport_table:
730 raise Exception("Missing inter-site transport - (%s)" %
731 cn_conn.transport_dnstr)
733 x_transport = self.transport_table[str(cn_conn.transport_guid)]
735 if t_repsFrom.transport_guid != x_transport.guid:
736 t_repsFrom.transport_guid = x_transport.guid
738 # See (NOTE MS-TECH INCORRECT) above
739 if x_transport.address_attr == "dNSHostName":
741 if t_repsFrom.version == 0x1:
742 if t_repsFrom.dns_name1 is None or \
743 t_repsFrom.dns_name1 != nastr:
744 t_repsFrom.dns_name1 = nastr
746 if t_repsFrom.dns_name1 is None or \
747 t_repsFrom.dns_name2 is None or \
748 t_repsFrom.dns_name1 != nastr or \
749 t_repsFrom.dns_name2 != nastr:
750 t_repsFrom.dns_name1 = nastr
751 t_repsFrom.dns_name2 = nastr
754 # MS tech specification says we retrieve the named
755 # attribute in "transportAddressAttribute" from the parent of
758 pdnstr = s_dsa.get_parent_dnstr()
759 attrs = [ x_transport.address_attr ]
761 res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
763 except ldb.LdbError, (enum, estr):
765 "Unable to find attr (%s) for (%s) - (%s)" %
766 (x_transport.address_attr, pdnstr, estr))
769 nastr = str(msg[x_transport.address_attr][0])
771 # See (NOTE MS-TECH INCORRECT) above
772 if t_repsFrom.version == 0x1:
773 if t_repsFrom.dns_name1 is None or \
774 t_repsFrom.dns_name1 != nastr:
775 t_repsFrom.dns_name1 = nastr
777 if t_repsFrom.dns_name1 is None or \
778 t_repsFrom.dns_name2 is None or \
779 t_repsFrom.dns_name1 != nastr or \
780 t_repsFrom.dns_name2 != nastr:
782 t_repsFrom.dns_name1 = nastr
783 t_repsFrom.dns_name2 = nastr
785 if t_repsFrom.is_modified():
786 logger.debug("modify_repsFrom(): %s" % t_repsFrom)
788 def is_repsFrom_implied(self, n_rep, cn_conn):
789 """Given a NC replica and NTDS Connection, determine if the connection
790 implies a repsFrom tuple should be present from the source DSA listed
791 in the connection to the naming context
793 :param n_rep: NC replica
794 :param conn: NTDS Connection
795 ::returns (True || False), source DSA:
797 # NTDS Connection must satisfy all the following criteria
798 # to imply a repsFrom tuple is needed:
800 # cn!enabledConnection = true.
801 # cn!options does not contain NTDSCONN_OPT_RODC_TOPOLOGY.
802 # cn!fromServer references an nTDSDSA object.
805 if cn_conn.is_enabled() and not cn_conn.is_rodc_topology():
807 s_dnstr = cn_conn.get_from_dnstr()
808 if s_dnstr is not None:
809 s_dsa = self.get_dsa(s_dnstr)
811 # No DSA matching this source DN string?
815 # To imply a repsFrom tuple is needed, each of these
818 # An NC replica of the NC "is present" on the DC to
819 # which the nTDSDSA object referenced by cn!fromServer
822 # An NC replica of the NC "should be present" on
824 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
826 if s_rep is None or not s_rep.is_present():
829 # To imply a repsFrom tuple is needed, each of these
832 # The NC replica on the DC referenced by cn!fromServer is
833 # a writable replica or the NC replica that "should be
834 # present" on the local DC is a partial replica.
836 # The NC is not a domain NC, the NC replica that
837 # "should be present" on the local DC is a partial
838 # replica, cn!transportType has no value, or
839 # cn!transportType has an RDN of CN=IP.
841 implied = (not s_rep.is_ro() or n_rep.is_partial()) and \
842 (not n_rep.is_domain() or
843 n_rep.is_partial() or
844 cn_conn.transport_dnstr is None or
845 cn_conn.transport_dnstr.find("CN=IP") == 0)
852 def translate_ntdsconn(self):
853 """This function adjusts values of repsFrom abstract attributes of NC
854 replicas on the local DC to match those implied by
855 nTDSConnection objects.
858 if self.my_dsa.is_translate_ntdsconn_disabled():
859 logger.debug("skipping translate_ntdsconn() because disabling flag is set")
862 logger.debug("translate_ntdsconn(): enter")
864 current_rep_table, needed_rep_table = self.my_dsa.get_rep_tables()
866 # Filled in with replicas we currently have that need deleting
869 # We're using the MS notation names here to allow
870 # correlation back to the published algorithm.
872 # n_rep - NC replica (n)
873 # t_repsFrom - tuple (t) in n!repsFrom
874 # s_dsa - Source DSA of the replica. Defined as nTDSDSA
875 # object (s) such that (s!objectGUID = t.uuidDsa)
876 # In our IDL representation of repsFrom the (uuidDsa)
877 # attribute is called (source_dsa_obj_guid)
878 # cn_conn - (cn) is nTDSConnection object and child of the local DC's
879 # nTDSDSA object and (cn!fromServer = s)
880 # s_rep - source DSA replica of n
882 # If we have the replica and its not needed
883 # then we add it to the "to be deleted" list.
884 for dnstr in current_rep_table:
885 if dnstr not in needed_rep_table:
886 delete_reps.add(dnstr)
889 DEBUG('current %d needed %d delete %d', len(current_rep_table),
890 len(needed_rep_table), len(delete_reps))
891 DEBUG('deleting these reps: %s', delete_reps)
892 for dnstr in delete_reps:
893 del current_rep_table[dnstr]
895 # Now perform the scan of replicas we'll need
896 # and compare any current repsFrom against the
898 for n_rep in needed_rep_table.values():
900 # load any repsFrom and fsmo roles as we'll
901 # need them during connection translation
902 n_rep.load_repsFrom(self.samdb)
903 n_rep.load_fsmo_roles(self.samdb)
905 # Loop thru the existing repsFrom tupples (if any)
906 for i, t_repsFrom in enumerate(n_rep.rep_repsFrom):
908 # for each tuple t in n!repsFrom, let s be the nTDSDSA
909 # object such that s!objectGUID = t.uuidDsa
910 guidstr = str(t_repsFrom.source_dsa_obj_guid)
911 s_dsa = self.get_dsa_by_guidstr(guidstr)
913 # Source dsa is gone from config (strange)
914 # so cleanup stale repsFrom for unlisted DSA
916 logger.warning("repsFrom source DSA guid (%s) not found" %
918 t_repsFrom.to_be_deleted = True
921 s_dnstr = s_dsa.dsa_dnstr
923 # Retrieve my DSAs connection object (if it exists)
924 # that specifies the fromServer equivalent to
925 # the DSA that is specified in the repsFrom source
926 cn_conn = self.my_dsa.get_connection_by_from_dnstr(s_dnstr)
928 # Let (cn) be the nTDSConnection object such that (cn)
929 # is a child of the local DC's nTDSDSA object and
930 # (cn!fromServer = s) and (cn!options) does not contain
931 # NTDSCONN_OPT_RODC_TOPOLOGY or NULL if no such (cn) exists.
932 if cn_conn and not cn_conn.is_rodc_topology():
935 # KCC removes this repsFrom tuple if any of the following
939 # No NC replica of the NC "is present" on DSA that
940 # would be source of replica
942 # A writable replica of the NC "should be present" on
943 # the local DC, but a partial replica "is present" on
945 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
947 if cn_conn is None or \
948 s_rep is None or not s_rep.is_present() or \
949 (not n_rep.is_ro() and s_rep.is_partial()):
951 t_repsFrom.to_be_deleted = True
954 # If the KCC did not remove t from n!repsFrom, it updates t
955 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
957 # Loop thru connections and add implied repsFrom tuples
958 # for each NTDSConnection under our local DSA if the
959 # repsFrom is not already present
960 for cn_conn in self.my_dsa.connect_table.values():
962 implied, s_dsa = self.is_repsFrom_implied(n_rep, cn_conn)
966 # Loop thru the existing repsFrom tupples (if any) and
967 # if we already have a tuple for this connection then
968 # no need to proceed to add. It will have been changed
969 # to have the correct attributes above
970 for t_repsFrom in n_rep.rep_repsFrom:
971 guidstr = str(t_repsFrom.source_dsa_obj_guid)
972 if s_dsa is self.get_dsa_by_guidstr(guidstr):
979 # Create a new RepsFromTo and proceed to modify
980 # it according to specification
981 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
983 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
985 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
987 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
989 # Add to our NC repsFrom as this is newly computed
990 if t_repsFrom.is_modified():
991 n_rep.rep_repsFrom.append(t_repsFrom)
994 # Display any to be deleted or modified repsFrom
995 text = n_rep.dumpstr_to_be_deleted()
997 logger.info("TO BE DELETED:\n%s" % text)
998 text = n_rep.dumpstr_to_be_modified()
1000 logger.info("TO BE MODIFIED:\n%s" % text)
1002 # Peform deletion from our tables but perform
1003 # no database modification
1004 n_rep.commit_repsFrom(self.samdb, ro=True)
1006 # Commit any modified repsFrom to the NC replica
1007 n_rep.commit_repsFrom(self.samdb)
1011 def merge_failed_links(self):
1012 """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
1013 The KCC on a writable DC attempts to merge the link and connection
1014 failure information from bridgehead DCs in its own site to help it
1015 identify failed bridgehead DCs.
1017 # MS-TECH Ref 6.2.2.3.2 Merge of kCCFailedLinks and kCCFailedLinks
1020 # 1. Queries every bridgehead server in your site (other than yourself)
1021 # 2. For every ntDSConnection that references a server in a different
1022 # site merge all the failure info
1024 # XXX - not implemented yet
1025 if opts.attempt_live_connections:
1026 DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1028 DEBUG("skipping merge_failed_links() because it requires real network connections\n"
1029 "and we weren't asked to --attempt-live-connctions")
1032 def setup_graph(self, part):
1033 """Set up a GRAPH, populated with a VERTEX for each site
1034 object, a MULTIEDGE for each siteLink object, and a
1035 MUTLIEDGESET for each siteLinkBridge object (or implied
1038 ::returns: a new graph
1042 g = IntersiteGraph()
1044 for site_guid, site in self.site_table.items():
1045 vertex = Vertex(site, part)
1046 vertex.guid = site_guid
1047 vertex.ndrpacked_guid = ndr_pack(site.site_guid)
1048 g.vertices.add(vertex)
1050 if not guid_to_vertex.get(site_guid):
1051 guid_to_vertex[site_guid] = []
1053 guid_to_vertex[site_guid].append(vertex)
1055 connected_vertices = set()
1056 for transport_guid, transport in self.transport_table.items():
1057 # Currently only ever "IP"
1058 for site_link_dn, site_link in self.sitelink_table.items():
1059 new_edge = create_edge(transport_guid, site_link, guid_to_vertex)
1060 connected_vertices.update(new_edge.vertices)
1061 g.edges.add(new_edge)
1063 # If 'Bridge all site links' is enabled and Win2k3 bridges required is not set
1064 # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1065 # No documentation for this however, ntdsapi.h appears to have listed:
1066 # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1067 if ((self.my_site.site_options & 0x00000002) == 0
1068 and (self.my_site.site_options & 0x00001000) == 0):
1069 g.edge_set.add(create_auto_edge_set(g, transport_guid))
1071 # TODO get all site link bridges
1072 for site_link_bridge in []:
1073 g.edge_set.add(create_edge_set(g, transport_guid,
1076 g.connected_vertices = connected_vertices
1078 #be less verbose in dot file output unless --debug
1079 do_dot_files = opts.dot_files and opts.debug
1081 for edge in g.edges:
1082 for a, b in itertools.combinations(edge.vertices, 2):
1083 dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1084 verify_properties = ()
1085 verify_and_dot('site_edges', dot_edges, directed=False, label=self.my_dsa_dnstr,
1086 properties=verify_properties, debug=DEBUG, verify=opts.verify,
1087 dot_files=do_dot_files)
1091 def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1092 """Get a bridghead DC.
1094 :param site: site object representing for which a bridgehead
1096 :param part: crossRef for NC to replicate.
1097 :param transport: interSiteTransport object for replication
1099 :param partial_ok: True if a DC containing a partial
1100 replica or a full replica will suffice, False if only
1101 a full replica will suffice.
1102 :param detect_failed: True to detect failed DCs and route
1103 replication traffic around them, False to assume no DC
1105 ::returns: dsa object for the bridgehead DC or None
1108 bhs = self.get_all_bridgeheads(site, part, transport,
1109 partial_ok, detect_failed)
1111 DEBUG_MAGENTA("get_bridgehead:\n\tsitedn=%s\n\tbhdn=None" %
1115 DEBUG_GREEN("get_bridgehead:\n\tsitedn=%s\n\tbhdn=%s" %
1116 (site.site_dnstr, bhs[0].dsa_dnstr))
1119 def get_all_bridgeheads(self, site, part, transport,
1120 partial_ok, detect_failed):
1121 """Get all bridghead DCs satisfying the given criteria
1123 :param site: site object representing the site for which
1124 bridgehead DCs are desired.
1125 :param part: partition for NC to replicate.
1126 :param transport: interSiteTransport object for
1127 replication traffic.
1128 :param partial_ok: True if a DC containing a partial
1129 replica or a full replica will suffice, False if
1130 only a full replica will suffice.
1131 :param detect_failed: True to detect failed DCs and route
1132 replication traffic around them, FALSE to assume
1134 ::returns: list of dsa object for available bridgehead
1140 logger.debug("get_all_bridgeheads: %s" % transport)
1141 if 'Site-5' in site.site_dnstr:
1142 DEBUG_RED("get_all_bridgeheads with %s, part%s, partial_ok %s"
1143 " detect_failed %s" % (site.site_dnstr, part.partstr,
1144 partial_ok, detect_failed))
1145 logger.debug(site.dsa_table)
1146 for key, dsa in site.dsa_table.items():
1148 pdnstr = dsa.get_parent_dnstr()
1150 # IF t!bridgeheadServerListBL has one or more values and
1151 # t!bridgeheadServerListBL does not contain a reference
1152 # to the parent object of dc then skip dc
1153 if (len(transport.bridgehead_list) != 0 and
1154 pdnstr not in transport.bridgehead_list):
1157 # IF dc is in the same site as the local DC
1158 # IF a replica of cr!nCName is not in the set of NC replicas
1159 # that "should be present" on dc or a partial replica of the
1160 # NC "should be present" but partialReplicasOkay = FALSE
1162 if self.my_site.same_site(dsa):
1163 needed, ro, partial = part.should_be_present(dsa)
1164 if not needed or (partial and not partial_ok):
1166 rep = dsa.get_current_replica(part.nc_dnstr)
1169 # IF an NC replica of cr!nCName is not in the set of NC
1170 # replicas that "are present" on dc or a partial replica of
1171 # the NC "is present" but partialReplicasOkay = FALSE
1174 rep = dsa.get_current_replica(part.nc_dnstr)
1175 if rep is None or (rep.is_partial() and not partial_ok):
1178 # IF AmIRODC() and cr!nCName corresponds to default NC then
1179 # Let dsaobj be the nTDSDSA object of the dc
1180 # IF dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1182 if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1183 if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1186 # IF t!name != "IP" and the parent object of dc has no value for
1187 # the attribute specified by t!transportAddressAttribute
1189 if transport.name != "IP":
1190 # MS tech specification says we retrieve the named
1191 # attribute in "transportAddressAttribute" from the parent
1194 attrs = [ transport.address_attr ]
1196 res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
1198 except ldb.LdbError, (enum, estr):
1202 if transport.address_attr not in msg:
1205 nastr = str(msg[transport.address_attr][0])
1207 # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1209 if self.is_bridgehead_failed(dsa, detect_failed):
1210 DEBUG("bridgehead is failed")
1213 logger.debug("get_all_bridgeheads: dsadn=%s" % dsa.dsa_dnstr)
1216 # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1218 # SORT bhs such that all GC servers precede DCs that are not GC
1219 # servers, and otherwise by ascending objectGUID
1221 # SORT bhs in a random order
1222 if site.is_random_bridgehead_disabled():
1223 bhs.sort(sort_dsa_by_gc_and_guid)
1230 def is_bridgehead_failed(self, dsa, detect_failed):
1231 """Determine whether a given DC is known to be in a failed state
1232 ::returns: True if and only if the DC should be considered failed
1234 Here we DEPART from the pseudo code spec which appears to be
1235 wrong. It says, in full:
1237 /***** BridgeheadDCFailed *****/
1238 /* Determine whether a given DC is known to be in a failed state.
1239 * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1240 * IN: detectFailedDCs - TRUE if and only failed DC detection is
1242 * RETURNS: TRUE if and only if the DC should be considered to be in a
1245 BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1247 IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1248 the options attribute of the site settings object for the local
1251 ELSEIF a tuple z exists in the kCCFailedLinks or
1252 kCCFailedConnections variables such that z.UUIDDsa =
1253 objectGUID, z.FailureCount > 1, and the current time -
1254 z.TimeFirstFailure > 2 hours
1257 RETURN detectFailedDCs
1261 where you will see detectFailedDCs is not behaving as
1262 advertised -- it is acting as a default return code in the
1263 event that a failure is not detected, not a switch turning
1264 detection on or off. Elsewhere the documentation seems to
1265 concur with the comment rather than the code.
1267 if not detect_failed:
1270 # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1271 # When DETECT_STALE_DISABLED, we can never know of if it's in a failed state
1272 if self.my_site.site_options & 0x00000008:
1275 return self.is_stale_link_connection(dsa)
1278 def create_connection(self, part, rbh, rsite, transport,
1279 lbh, lsite, link_opt, link_sched,
1280 partial_ok, detect_failed):
1281 """Create an nTDSConnection object with the given parameters
1282 if one does not already exist.
1284 :param part: crossRef object for the NC to replicate.
1285 :param rbh: nTDSDSA object for DC to act as the
1286 IDL_DRSGetNCChanges server (which is in a site other
1287 than the local DC's site).
1288 :param rsite: site of the rbh
1289 :param transport: interSiteTransport object for the transport
1290 to use for replication traffic.
1291 :param lbh: nTDSDSA object for DC to act as the
1292 IDL_DRSGetNCChanges client (which is in the local DC's site).
1293 :param lsite: site of the lbh
1294 :param link_opt: Replication parameters (aggregated siteLink options, etc.)
1295 :param link_sched: Schedule specifying the times at which
1296 to begin replicating.
1297 :partial_ok: True if bridgehead DCs containing partial
1298 replicas of the NC are acceptable.
1299 :param detect_failed: True to detect failed DCs and route
1300 replication traffic around them, FALSE to assume no DC
1303 rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1305 rbh_table = {x.dsa_dnstr:x for x in rbhs_all}
1307 DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all), [x.dsa_dnstr for x in rbhs_all]))
1309 # MS-TECH says to compute rbhs_avail but then doesn't use it
1310 # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1311 # partial_ok, detect_failed)
1313 lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1316 DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all), [x.dsa_dnstr for x in lbhs_all]))
1318 # MS-TECH says to compute lbhs_avail but then doesn't use it
1319 # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1320 # partial_ok, detect_failed)
1322 # FOR each nTDSConnection object cn such that the parent of cn is
1323 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1324 for ldsa in lbhs_all:
1325 for cn in ldsa.connect_table.values():
1327 rdsa = rbh_table.get(cn.from_dnstr)
1331 DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1332 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1333 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1334 # cn!transportType references t
1335 if (cn.is_generated() and not cn.is_rodc_topology() and
1336 cn.transport_guid == transport.guid):
1338 # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1339 # cn!options and cn!schedule != sch
1340 # Perform an originating update to set cn!schedule to
1342 if (not cn.is_user_owned_schedule() and
1343 not cn.is_equivalent_schedule(link_sched)):
1344 cn.schedule = link_sched
1345 cn.set_modified(True)
1347 # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1348 # NTDSCONN_OPT_USE_NOTIFY are set in cn
1349 if cn.is_override_notify_default() and \
1352 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1354 # Perform an originating update to clear bits
1355 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1356 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1357 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1359 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1360 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1361 cn.set_modified(True)
1366 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1368 # Perform an originating update to set bits
1369 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1370 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1371 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1373 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1374 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1375 cn.set_modified(True)
1378 # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1379 if cn.is_twoway_sync():
1381 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1383 # Perform an originating update to clear bit
1384 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1385 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1386 cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1387 cn.set_modified(True)
1392 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1394 # Perform an originating update to set bit
1395 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1396 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1397 cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1398 cn.set_modified(True)
1401 # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1403 if cn.is_intersite_compression_disabled():
1405 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1407 # Perform an originating update to clear bit
1408 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1411 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0:
1413 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1414 cn.set_modified(True)
1418 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1420 # Perform an originating update to set bit
1421 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1424 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0:
1426 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1427 cn.set_modified(True)
1429 # Display any modified connection
1431 if cn.to_be_modified:
1432 logger.info("TO BE MODIFIED:\n%s" % cn)
1434 ldsa.commit_connections(self.samdb, ro=True)
1436 ldsa.commit_connections(self.samdb)
1439 valid_connections = 0
1441 # FOR each nTDSConnection object cn such that cn!parent is
1442 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1443 for ldsa in lbhs_all:
1444 for cn in ldsa.connect_table.values():
1446 rdsa = rbh_table.get(cn.from_dnstr)
1450 DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1452 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1453 # cn!transportType references t) and
1454 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1455 if ((not cn.is_generated() or
1456 cn.transport_guid == transport.guid) and
1457 not cn.is_rodc_topology()):
1459 # LET rguid be the objectGUID of the nTDSDSA object
1460 # referenced by cn!fromServer
1461 # LET lguid be (cn!parent)!objectGUID
1463 # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1464 # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1465 # Increment cValidConnections by 1
1466 if (not self.is_bridgehead_failed(rdsa, detect_failed) and
1467 not self.is_bridgehead_failed(ldsa, detect_failed)):
1468 valid_connections += 1
1470 # IF keepConnections does not contain cn!objectGUID
1471 # APPEND cn!objectGUID to keepConnections
1472 self.kept_connections.add(cn)
1475 DEBUG_RED("valid connections %d" % valid_connections)
1476 DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1477 # IF cValidConnections = 0
1478 if valid_connections == 0:
1480 # LET opt be NTDSCONN_OPT_IS_GENERATED
1481 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1483 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1484 # SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1485 # NTDSCONN_OPT_USE_NOTIFY in opt
1486 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1487 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1488 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1490 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1491 # SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1492 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1493 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1495 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1497 # SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1499 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0:
1500 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1502 # Perform an originating update to create a new nTDSConnection
1503 # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1504 # cn!options = opt, cn!transportType is a reference to t,
1505 # cn!fromServer is a reference to rbh, and cn!schedule = sch
1506 cn = lbh.new_connection(opt, 0, transport, rbh.dsa_dnstr, link_sched)
1508 # Display any added connection
1511 logger.info("TO BE ADDED:\n%s" % cn)
1513 lbh.commit_connections(self.samdb, ro=True)
1515 lbh.commit_connections(self.samdb)
1517 # APPEND cn!objectGUID to keepConnections
1518 self.kept_connections.add(cn)
1520 def add_transports(self, vertex, local_vertex, graph, detect_failed):
1522 # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1523 # here and in the, but using vertex seems to make more
1524 # sense. That is, it wants this:
1526 #bh = self.get_bridgehead(vertex.site, vertex.part, transport,
1527 # local_vertex.is_black(), detect_failed)
1531 vertex.accept_red_red = []
1532 vertex.accept_black = []
1533 found_failed = False
1534 for t_guid, transport in self.transport_table.items():
1535 if transport.name != 'IP':
1536 #XXX well this is cheating a bit
1537 logging.warning("WARNING: we are ignoring a transport named %r" % transport.name)
1540 # FLAG_CR_NTDS_DOMAIN 0x00000002
1541 if (vertex.is_red() and transport.name != "IP" and
1542 vertex.part.system_flags & 0x00000002):
1545 if vertex not in graph.connected_vertices:
1548 partial_replica_okay = vertex.is_black()
1549 bh = self.get_bridgehead(vertex.site, vertex.part, transport,
1550 partial_replica_okay, detect_failed)
1555 vertex.accept_red_red.append(t_guid)
1556 vertex.accept_black.append(t_guid)
1558 # Add additional transport to allow another run of Dijkstra
1559 vertex.accept_red_red.append("EDGE_TYPE_ALL")
1560 vertex.accept_black.append("EDGE_TYPE_ALL")
1564 def create_connections(self, graph, part, detect_failed):
1565 """Construct an NC replica graph for the NC identified by
1566 the given crossRef, then create any additional nTDSConnection
1569 :param graph: site graph.
1570 :param part: crossRef object for NC.
1571 :param detect_failed: True to detect failed DCs and route
1572 replication traffic around them, False to assume no DC
1575 Modifies self.kept_connections by adding any connections
1576 deemed to be "in use".
1578 ::returns: (all_connected, found_failed_dc)
1579 (all_connected) True if the resulting NC replica graph
1580 connects all sites that need to be connected.
1581 (found_failed_dc) True if one or more failed DCs were
1584 all_connected = True
1585 found_failed = False
1587 logger.debug("create_connections(): enter\n\tpartdn=%s\n\tdetect_failed=%s" %
1588 (part.nc_dnstr, detect_failed))
1590 # XXX - This is a highly abbreviated function from the MS-TECH
1591 # ref. It creates connections between bridgeheads to all
1592 # sites that have appropriate replicas. Thus we are not
1593 # creating a minimum cost spanning tree but instead
1594 # producing a fully connected tree. This should produce
1595 # a full (albeit not optimal cost) replication topology.
1597 my_vertex = Vertex(self.my_site, part)
1598 my_vertex.color_vertex()
1600 for v in graph.vertices:
1602 if self.add_transports(v, my_vertex, graph, False):
1605 # No NC replicas for this NC in the site of the local DC,
1606 # so no nTDSConnection objects need be created
1607 if my_vertex.is_white():
1608 return all_connected, found_failed
1610 edge_list, component_count = self.get_spanning_tree_edges(graph, label=part.partstr)
1612 logger.debug("%s Number of components: %d" % (part.nc_dnstr, component_count))
1613 if component_count > 1:
1614 all_connected = False
1616 # LET partialReplicaOkay be TRUE if and only if
1617 # localSiteVertex.Color = COLOR.BLACK
1618 if my_vertex.is_black():
1623 # Utilize the IP transport only for now
1625 for transport in self.transport_table.values():
1626 if transport.name == "IP":
1629 if transport is None:
1630 raise Exception("Unable to find inter-site transport for IP")
1632 DEBUG("edge_list %s" % edge_list)
1634 if e.directed and e.vertices[0].site is self.my_site: # more accurate comparison?
1637 if e.vertices[0].site is self.my_site:
1638 rsite = e.vertices[1].site
1640 rsite = e.vertices[0].site
1642 # We don't make connections to our own site as that
1643 # is intrasite topology generator's job
1644 if rsite is self.my_site:
1645 DEBUG("rsite is my_site")
1648 # Determine bridgehead server in remote site
1649 rbh = self.get_bridgehead(rsite, part, transport,
1650 partial_ok, detect_failed)
1654 # RODC acts as an BH for itself
1656 # LET lbh be the nTDSDSA object of the local DC
1658 # LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1659 # cr, t, partialReplicaOkay, detectFailedDCs)
1660 if self.my_dsa.is_ro():
1661 lsite = self.my_site
1664 lsite = self.my_site
1665 lbh = self.get_bridgehead(lsite, part, transport,
1666 partial_ok, detect_failed)
1669 DEBUG_RED("DISASTER! lbh is None")
1674 DEBUG_BLUE("vertices")
1676 DEBUG_BLUE("bridgeheads")
1678 DEBUG_BLUE("-" * 70)
1680 sitelink = e.site_link
1681 if sitelink is None:
1685 link_opt = sitelink.options
1686 link_sched = sitelink.schedule
1688 self.create_connection(part, rbh, rsite, transport,
1689 lbh, lsite, link_opt, link_sched,
1690 partial_ok, detect_failed)
1692 return all_connected, found_failed
1694 def create_intersite_connections(self):
1695 """Computes an NC replica graph for each NC replica that "should be
1696 present" on the local DC or "is present" on any DC in the same site
1697 as the local DC. For each edge directed to an NC replica on such a
1698 DC from an NC replica on a DC in another site, the KCC creates an
1699 nTDSConnection object to imply that edge if one does not already
1702 Modifies self.kept_connections - A set of nTDSConnection
1703 objects for edges that are directed
1704 to the local DC's site in one or more NC replica graphs.
1706 returns: True if spanning trees were created for all NC replica
1707 graphs, otherwise False.
1709 all_connected = True
1710 self.kept_connections = set()
1712 # LET crossRefList be the set containing each object o of class
1713 # crossRef such that o is a child of the CN=Partitions child of the
1716 # FOR each crossRef object cr in crossRefList
1717 # IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1718 # is clear in cr!systemFlags, skip cr.
1719 # LET g be the GRAPH return of SetupGraph()
1721 for part in self.part_table.values():
1723 if not part.is_enabled():
1726 if part.is_foreign():
1729 graph = self.setup_graph(part)
1731 # Create nTDSConnection objects, routing replication traffic
1732 # around "failed" DCs.
1733 found_failed = False
1735 connected, found_failed = self.create_connections(graph, part, True)
1737 DEBUG("with detect_failed: connected %s Found failed %s" % (connected, found_failed))
1739 all_connected = False
1742 # One or more failed DCs preclude use of the ideal NC
1743 # replica graph. Add connections for the ideal graph.
1744 self.create_connections(graph, part, False)
1746 return all_connected
1748 def get_spanning_tree_edges(self, graph, label=None):
1749 # Phase 1: Run Dijkstra's to get a list of internal edges, which are
1750 # just the shortest-paths connecting colored vertices
1752 internal_edges = set()
1754 for e_set in graph.edge_set:
1756 for v in graph.vertices:
1759 # All con_type in an edge set is the same
1760 for e in e_set.edges:
1761 edgeType = e.con_type
1762 for v in e.vertices:
1765 if opts.verify or opts.dot_files:
1766 graph_edges = [(a.site.site_dnstr, b.site.site_dnstr)
1767 for a, b in itertools.chain(*(itertools.combinations(edge.vertices, 2)
1768 for edge in e_set.edges))]
1769 graph_nodes = [v.site.site_dnstr for v in graph.vertices]
1771 if opts.dot_files and opts.debug:
1772 write_dot_file('edgeset_%s' % (edgeType,), graph_edges, vertices=graph_nodes,
1776 verify_graph('spanning tree edge set %s' % edgeType, graph_edges, vertices=graph_nodes,
1777 properties=('complete', 'connected'), debug=DEBUG)
1779 # Run dijkstra's algorithm with just the red vertices as seeds
1780 # Seed from the full replicas
1781 dijkstra(graph, edgeType, False)
1784 process_edge_set(graph, e_set, internal_edges)
1786 # Run dijkstra's algorithm with red and black vertices as the seeds
1787 # Seed from both full and partial replicas
1788 dijkstra(graph, edgeType, True)
1791 process_edge_set(graph, e_set, internal_edges)
1793 # All vertices have root/component as itself
1794 setup_vertices(graph)
1795 process_edge_set(graph, None, internal_edges)
1797 if opts.verify or opts.dot_files:
1798 graph_edges = [(e.v1.site.site_dnstr, e.v2.site.site_dnstr) for e in internal_edges]
1799 graph_nodes = [v.site.site_dnstr for v in graph.vertices]
1800 verify_properties = ('multi_edge_forest',)
1801 verify_and_dot('prekruskal', graph_edges, graph_nodes, label=label,
1802 properties=verify_properties, debug=DEBUG, verify=opts.verify,
1803 dot_files=opts.dot_files)
1806 # Phase 2: Run Kruskal's on the internal edges
1807 output_edges, components = kruskal(graph, internal_edges)
1809 # This recalculates the cost for the path connecting the closest red vertex
1810 # Ignoring types is fine because NO suboptimal edge should exist in the graph
1811 dijkstra(graph, "EDGE_TYPE_ALL", False) # TODO rename
1812 # Phase 3: Process the output
1813 for v in graph.vertices:
1817 v.dist_to_red = v.repl_info.cost
1819 if opts.verify or opts.dot_files:
1820 graph_edges = [(e.v1.site.site_dnstr, e.v2.site.site_dnstr) for e in internal_edges]
1821 graph_nodes = [v.site.site_dnstr for v in graph.vertices]
1822 verify_properties = ('multi_edge_forest',)
1823 verify_and_dot('postkruskal', graph_edges, graph_nodes, label=label,
1824 properties=verify_properties, debug=DEBUG, verify=opts.verify,
1825 dot_files=opts.dot_files)
1827 # count the components
1828 return self.copy_output_edges(graph, output_edges), components
1830 # This ensures only one-way connections for partial-replicas
1831 def copy_output_edges(self, graph, output_edges):
1833 vid = self.my_site # object guid for the local dc's site
1835 for edge in output_edges:
1836 # Three-way edges are no problem here since these were created by
1837 # add_out_edge which only has two endpoints
1838 v = edge.vertices[0]
1839 w = edge.vertices[1]
1840 if v.site is vid or w.site is vid:
1841 if (v.is_black() or w.is_black()) and not v.dist_to_red == MAX_DWORD:
1842 edge.directed = True
1844 if w.dist_to_red < v.dist_to_red:
1845 edge.vertices[0] = w
1846 edge.vertices[1] = v
1848 edge_list.append(edge)
1852 def intersite(self):
1853 """The head method for generating the inter-site KCC replica
1854 connection graph and attendant nTDSConnection objects
1857 Produces self.kept_connections set of NTDS Connections
1858 that should be kept during subsequent pruning process.
1860 ::return (True or False): (True) if the produced NC replica
1861 graph connects all sites that need to be connected
1866 mysite = self.my_site
1867 all_connected = True
1869 logger.debug("intersite(): enter")
1871 # Determine who is the ISTG
1873 mysite.select_istg(self.samdb, mydsa, ro=True)
1875 mysite.select_istg(self.samdb, mydsa, ro=False)
1877 # Test whether local site has topology disabled
1878 if mysite.is_intersite_topology_disabled():
1879 logger.debug("intersite(): exit disabled all_connected=%d" %
1881 return all_connected
1883 if not mydsa.is_istg():
1884 logger.debug("intersite(): exit not istg all_connected=%d" %
1886 return all_connected
1888 self.merge_failed_links()
1890 # For each NC with an NC replica that "should be present" on the
1891 # local DC or "is present" on any DC in the same site as the
1892 # local DC, the KCC constructs a site graph--a precursor to an NC
1893 # replica graph. The site connectivity for a site graph is defined
1894 # by objects of class interSiteTransport, siteLink, and
1895 # siteLinkBridge in the config NC.
1897 all_connected = self.create_intersite_connections()
1899 logger.debug("intersite(): exit all_connected=%d" % all_connected)
1900 return all_connected
1902 def update_rodc_connection(self):
1903 """Runs when the local DC is an RODC and updates the RODC NTFRS
1906 # Given an nTDSConnection object cn1, such that cn1.options contains
1907 # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1908 # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1909 # that the following is true:
1911 # cn1.fromServer = cn2.fromServer
1912 # cn1.schedule = cn2.schedule
1914 # If no such cn2 can be found, cn1 is not modified.
1915 # If no such cn1 can be found, nothing is modified by this task.
1917 if not self.my_dsa.is_ro():
1920 all_connections = self.my_dsa.connect_table.values()
1921 ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1922 rw_connections = [x for x in all_connections if x not in ro_connections]
1924 # XXX here we are dealing with multiple RODC_TOPO connections,
1925 # if they exist. It is not clear whether the spec means that
1926 # or if it ever arises.
1927 if rw_connections and ro_connections:
1928 for con in ro_connections:
1929 cn2 = rw_connections[0]
1930 con.from_dnstr = cn2.from_dnstr
1931 con.schedule = cn2.schedule
1932 con.to_be_modified = True
1934 self.my_dsa.commit_connections(self.samdb, ro=opts.readonly)
1936 def intrasite_max_node_edges(self, node_count):
1937 """Returns the maximum number of edges directed to a node in
1938 the intrasite replica graph.
1940 The KCC does not create more
1941 than 50 edges directed to a single DC. To optimize replication,
1942 we compute that each node should have n+2 total edges directed
1943 to it such that (n) is the smallest non-negative integer
1944 satisfying (node_count <= 2*(n*n) + 6*n + 7)
1946 (If the number of edges is m (i.e. n + 2), that is the same as
1947 2 * m*m - 2 * m + 3).
1957 :param node_count: total number of nodes in the replica graph
1961 if node_count <= (2 * (n * n) + (6 * n) + 7):
1969 def construct_intrasite_graph(self, site_local, dc_local,
1970 nc_x, gc_only, detect_stale):
1972 # We're using the MS notation names here to allow
1973 # correlation back to the published algorithm.
1975 # nc_x - naming context (x) that we are testing if it
1976 # "should be present" on the local DC
1977 # f_of_x - replica (f) found on a DC (s) for NC (x)
1978 # dc_s - DC where f_of_x replica was found
1979 # dc_local - local DC that potentially needs a replica
1981 # r_list - replica list R
1982 # p_of_x - replica (p) is partial and found on a DC (s)
1984 # l_of_x - replica (l) is the local replica for NC (x)
1985 # that should appear on the local DC
1986 # r_len = is length of replica list |R|
1988 # If the DSA doesn't need a replica for this
1989 # partition (NC x) then continue
1990 needed, ro, partial = nc_x.should_be_present(dc_local)
1992 DEBUG_YELLOW("construct_intrasite_graph(): enter" +
1993 "\n\tgc_only=%d" % gc_only +
1994 "\n\tdetect_stale=%d" % detect_stale +
1995 "\n\tneeded=%s" % needed +
1997 "\n\tpartial=%s" % partial +
2001 DEBUG_RED("%s lacks 'should be present' status, aborting construct_intersite_graph!" %
2005 # Create a NCReplica that matches what the local replica
2006 # should say. We'll use this below in our r_list
2007 l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid,
2010 l_of_x.identify_by_basedn(self.samdb)
2012 l_of_x.rep_partial = partial
2015 # Add this replica that "should be present" to the
2016 # needed replica table for this DSA
2017 dc_local.add_needed_replica(l_of_x)
2021 # Let R be a sequence containing each writable replica f of x
2022 # such that f "is present" on a DC s satisfying the following
2025 # * s is a writable DC other than the local DC.
2027 # * s is in the same site as the local DC.
2029 # * If x is a read-only full replica and x is a domain NC,
2030 # then the DC's functional level is at least
2031 # DS_BEHAVIOR_WIN2008.
2033 # * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
2034 # in the options attribute of the site settings object for
2035 # the local DC's site, or no tuple z exists in the
2036 # kCCFailedLinks or kCCFailedConnections variables such
2037 # that z.UUIDDsa is the objectGUID of the nTDSDSA object
2038 # for s, z.FailureCount > 0, and the current time -
2039 # z.TimeFirstFailure > 2 hours.
2043 # We'll loop thru all the DSAs looking for
2044 # writeable NC replicas that match the naming
2045 # context dn for (nc_x)
2047 for dc_s in self.my_site.dsa_table.values():
2048 # If this partition (nc_x) doesn't appear as a
2049 # replica (f_of_x) on (dc_s) then continue
2050 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2053 # Pull out the NCReplica (f) of (x) with the dn
2054 # that matches NC (x) we are examining.
2055 f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2057 # Replica (f) of NC (x) must be writable
2061 # Replica (f) of NC (x) must satisfy the
2062 # "is present" criteria for DC (s) that
2064 if not f_of_x.is_present():
2067 # DC (s) must be a writable DSA other than
2068 # my local DC. In other words we'd only replicate
2069 # from other writable DC
2070 if dc_s.is_ro() or dc_s is dc_local:
2073 # Certain replica graphs are produced only
2074 # for global catalogs, so test against
2075 # method input parameter
2076 if gc_only and not dc_s.is_gc():
2079 # DC (s) must be in the same site as the local DC
2080 # as this is the intra-site algorithm. This is
2081 # handled by virtue of placing DSAs in per
2082 # site objects (see enclosing for() loop)
2084 # If NC (x) is intended to be read-only full replica
2085 # for a domain NC on the target DC then the source
2086 # DC should have functional level at minimum WIN2008
2088 # Effectively we're saying that in order to replicate
2089 # to a targeted RODC (which was introduced in Windows 2008)
2090 # then we have to replicate from a DC that is also minimally
2093 # You can also see this requirement in the MS special
2094 # considerations for RODC which state that to deploy
2095 # an RODC, at least one writable domain controller in
2096 # the domain must be running Windows Server 2008
2097 if ro and not partial and nc_x.nc_type == NCType.domain:
2098 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2101 # If we haven't been told to turn off stale connection
2102 # detection and this dsa has a stale connection then
2104 if detect_stale and self.is_stale_link_connection(dc_s):
2107 # Replica meets criteria. Add it to table indexed
2108 # by the GUID of the DC that it appears on
2109 r_list.append(f_of_x)
2111 # If a partial (not full) replica of NC (x) "should be present"
2112 # on the local DC, append to R each partial replica (p of x)
2113 # such that p "is present" on a DC satisfying the same
2114 # criteria defined above for full replica DCs.
2116 # XXX This loop and the previous one differ only in whether
2117 # the replica is partial or not. here we only accept partial
2118 # (because we're partial); before we only accepted full. Order
2119 # doen't matter (the list is sorted a few lines down) so these
2120 # loops could easily be merged. Or this could be a helper
2124 # Now we loop thru all the DSAs looking for
2125 # partial NC replicas that match the naming
2126 # context dn for (NC x)
2127 for dc_s in self.my_site.dsa_table.values():
2129 # If this partition NC (x) doesn't appear as a
2130 # replica (p) of NC (x) on the dsa DC (s) then
2132 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2135 # Pull out the NCReplica with the dn that
2136 # matches NC (x) we are examining.
2137 p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2139 # Replica (p) of NC (x) must be partial
2140 if not p_of_x.is_partial():
2143 # Replica (p) of NC (x) must satisfy the
2144 # "is present" criteria for DC (s) that
2146 if not p_of_x.is_present():
2149 # DC (s) must be a writable DSA other than
2150 # my DSA. In other words we'd only replicate
2151 # from other writable DSA
2152 if dc_s.is_ro() or dc_s is dc_local:
2155 # Certain replica graphs are produced only
2156 # for global catalogs, so test against
2157 # method input parameter
2158 if gc_only and not dc_s.is_gc():
2161 # DC (s) must be in the same site as the local DC
2162 # as this is the intra-site algorithm. This is
2163 # handled by virtue of placing DSAs in per
2164 # site objects (see enclosing for() loop)
2166 # This criteria is moot (a no-op) for this case
2167 # because we are scanning for (partial = True). The
2168 # MS algorithm statement says partial replica scans
2169 # should adhere to the "same" criteria as full replica
2170 # scans so the criteria doesn't change here...its just
2171 # rendered pointless.
2173 # The case that is occurring would be a partial domain
2174 # replica is needed on a local DC global catalog. There
2175 # is no minimum windows behavior for those since GCs
2176 # have always been present.
2177 if ro and not partial and nc_x.nc_type == NCType.domain:
2178 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2181 # If we haven't been told to turn off stale connection
2182 # detection and this dsa has a stale connection then
2184 if detect_stale and self.is_stale_link_connection(dc_s):
2187 # Replica meets criteria. Add it to table indexed
2188 # by the GUID of the DSA that it appears on
2189 r_list.append(p_of_x)
2191 # Append to R the NC replica that "should be present"
2193 r_list.append(l_of_x)
2195 r_list.sort(sort_replica_by_dsa_guid)
2198 max_node_edges = self.intrasite_max_node_edges(r_len)
2200 # Add a node for each r_list element to the replica graph
2203 node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2204 graph_list.append(node)
2206 # For each r(i) from (0 <= i < |R|-1)
2208 while i < (r_len-1):
2209 # Add an edge from r(i) to r(i+1) if r(i) is a full
2210 # replica or r(i+1) is a partial replica
2211 if not r_list[i].is_partial() or r_list[i+1].is_partial():
2212 graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
2214 # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2215 # replica or ri is a partial replica.
2216 if not r_list[i+1].is_partial() or r_list[i].is_partial():
2217 graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
2220 # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2221 # or r0 is a partial replica.
2222 if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
2223 graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
2225 # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2226 # r|R|-1 is a partial replica.
2227 if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
2228 graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
2230 DEBUG("r_list is length %s" % len(r_list))
2231 DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr)) for x in r_list))
2233 do_dot_files = opts.dot_files and opts.debug
2234 if opts.verify or do_dot_files:
2236 dot_vertices = set()
2237 for v1 in graph_list:
2238 dot_vertices.add(v1.dsa_dnstr)
2239 for v2 in v1.edge_from:
2240 dot_edges.append((v2, v1.dsa_dnstr))
2241 dot_vertices.add(v2)
2243 verify_properties = ('connected', 'directed_double_ring')
2244 verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2245 label='%s__%s__%s' % (site_local.site_dnstr, nctype_lut[nc_x.nc_type], nc_x.nc_dnstr),
2246 properties=verify_properties, debug=DEBUG, verify=opts.verify,
2247 dot_files=do_dot_files, directed=True)
2251 # For each existing nTDSConnection object implying an edge
2252 # from rj of R to ri such that j != i, an edge from rj to ri
2253 # is not already in the graph, and the total edges directed
2254 # to ri is less than n+2, the KCC adds that edge to the graph.
2255 for vertex in graph_list:
2256 dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2257 for connect in dsa.connect_table.values():
2258 remote = connect.from_dnstr
2259 if remote in self.my_site.dsa_table:
2260 vertex.add_edge_from(remote)
2262 DEBUG('reps are: %s' % ' '.join(x.rep_dsa_dnstr for x in r_list))
2263 DEBUG('dsas are: %s' % ' '.join(x.dsa_dnstr for x in graph_list))
2265 for tnode in graph_list:
2266 # To optimize replication latency in sites with many NC replicas, the
2267 # KCC adds new edges directed to ri to bring the total edges to n+2,
2268 # where the NC replica rk of R from which the edge is directed
2269 # is chosen at random such that k != i and an edge from rk to ri
2270 # is not already in the graph.
2272 # Note that the KCC tech ref does not give a number for the definition
2273 # of "sites with many NC replicas". At a bare minimum to satisfy
2274 # n+2 edges directed at a node we have to have at least three replicas
2275 # in |R| (i.e. if n is zero then at least replicas from two other graph
2276 # nodes may direct edges to us).
2277 if r_len >= 3 and not tnode.has_sufficient_edges():
2278 candidates = [x for x in graph_list if (x is not tnode and
2279 x.dsa_dnstr not in tnode.edge_from)]
2281 DEBUG_BLUE("looking for random link for %s. r_len %d, graph len %d candidates %d"
2282 % (tnode.dsa_dnstr, r_len, len(graph_list), len(candidates)))
2284 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2286 while candidates and not tnode.has_sufficient_edges():
2287 other = random.choice(candidates)
2288 DEBUG("trying to add candidate %s" % other.dsa_dstr)
2289 if not tnode.add_edge_from(other):
2290 DEBUG_RED("could not add %s" % other.dsa_dstr)
2291 candidates.remove(other)
2293 DEBUG_CYAN("not adding links to %s: nodes %s, links is %s/%s" %
2294 (tnode.dsa_dnstr, r_len, len(tnode.edge_from), tnode.max_edges))
2297 # Print the graph node in debug mode
2298 logger.debug("%s" % tnode)
2300 # For each edge directed to the local DC, ensure a nTDSConnection
2301 # points to us that satisfies the KCC criteria
2303 if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2304 tnode.add_connections_from_edges(dc_local)
2307 if opts.verify or do_dot_files:
2309 dot_vertices = set()
2310 for v1 in graph_list:
2311 dot_vertices.add(v1.dsa_dnstr)
2312 for v2 in v1.edge_from:
2313 dot_edges.append((v2, v1.dsa_dnstr))
2314 dot_vertices.add(v2)
2316 verify_properties = ('connected', 'directed_double_ring_or_small')
2317 verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2318 label='%s__%s__%s' % (site_local.site_dnstr, nctype_lut[nc_x.nc_type], nc_x.nc_dnstr),
2319 properties=verify_properties, debug=DEBUG, verify=opts.verify,
2320 dot_files=do_dot_files, directed=True)
2323 def intrasite(self):
2324 """The head method for generating the intra-site KCC replica
2325 connection graph and attendant nTDSConnection objects
2331 logger.debug("intrasite(): enter")
2333 # Test whether local site has topology disabled
2334 mysite = self.my_site
2335 if mysite.is_intrasite_topology_disabled():
2338 detect_stale = (not mysite.is_detect_stale_disabled())
2339 for connect in mydsa.connect_table.values():
2340 if connect.to_be_added:
2341 DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2343 # Loop thru all the partitions, with gc_only False
2344 for partdn, part in self.part_table.items():
2345 self.construct_intrasite_graph(mysite, mydsa, part, False,
2347 for connect in mydsa.connect_table.values():
2348 if connect.to_be_added:
2349 DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2352 # If the DC is a GC server, the KCC constructs an additional NC
2353 # replica graph (and creates nTDSConnection objects) for the
2354 # config NC as above, except that only NC replicas that "are present"
2355 # on GC servers are added to R.
2356 for connect in mydsa.connect_table.values():
2357 if connect.to_be_added:
2358 DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2360 # Do it again, with gc_only True
2361 for partdn, part in self.part_table.items():
2362 if part.is_config():
2363 self.construct_intrasite_graph(mysite, mydsa, part, True,
2366 # The DC repeats the NC replica graph computation and nTDSConnection
2367 # creation for each of the NC replica graphs, this time assuming
2368 # that no DC has failed. It does so by re-executing the steps as
2369 # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2370 # set in the options attribute of the site settings object for
2371 # the local DC's site. (ie. we set "detec_stale" flag to False)
2372 for connect in mydsa.connect_table.values():
2373 if connect.to_be_added:
2374 DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2376 # Loop thru all the partitions.
2377 for partdn, part in self.part_table.items():
2378 self.construct_intrasite_graph(mysite, mydsa, part, False,
2379 False) # don't detect stale
2381 # If the DC is a GC server, the KCC constructs an additional NC
2382 # replica graph (and creates nTDSConnection objects) for the
2383 # config NC as above, except that only NC replicas that "are present"
2384 # on GC servers are added to R.
2385 for connect in mydsa.connect_table.values():
2386 if connect.to_be_added:
2387 DEBUG_RED("TO BE ADDED:\n%s" % connect)
2389 for partdn, part in self.part_table.items():
2390 if part.is_config():
2391 self.construct_intrasite_graph(mysite, mydsa, part, True,
2392 False) # don't detect stale
2395 # Display any to be added or modified repsFrom
2396 for connect in mydsa.connect_table.values():
2397 if connect.to_be_deleted:
2398 logger.info("TO BE DELETED:\n%s" % connect)
2399 if connect.to_be_modified:
2400 logger.info("TO BE MODIFIED:\n%s" % connect)
2401 if connect.to_be_added:
2402 DEBUG_GREEN("TO BE ADDED:\n%s" % connect)
2404 mydsa.commit_connections(self.samdb, ro=True)
2406 # Commit any newly created connections to the samdb
2407 mydsa.commit_connections(self.samdb)
2410 def list_dsas(self):
2414 self.load_all_sites()
2415 self.load_all_partitions()
2416 self.load_all_transports()
2417 self.load_all_sitelinks()
2419 for site in self.site_table.values():
2420 dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2421 for dsa in site.dsa_table.values()])
2424 def load_samdb(self, dburl, lp, creds):
2425 self.samdb = SamDB(url=dburl,
2426 session_info=system_session(),
2427 credentials=creds, lp=lp)
2431 def plot_all_connections(self, basename, verify_properties=()):
2432 verify = verify_properties and opts.verify
2433 plot = opts.dot_files
2434 if not (verify or plot):
2442 for dsa in self.dsa_by_dnstr.values():
2443 dot_vertices.append(dsa.dsa_dnstr)
2445 vertex_colours.append('#cc0000')
2447 vertex_colours.append('#0000cc')
2448 for con in dsa.connect_table.values():
2449 if con.is_rodc_topology():
2450 edge_colours.append('red')
2452 edge_colours.append('blue')
2453 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2455 verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2456 label=self.my_dsa_dnstr, properties=verify_properties,
2457 debug=DEBUG, verify=verify, dot_files=plot,
2458 directed=True, edge_colors=edge_colours,
2459 vertex_colors=vertex_colours)
2462 def run(self, dburl, lp, creds, forced_local_dsa=None,
2463 forget_local_links=False, forget_intersite_links=False):
2464 """Method to perform a complete run of the KCC and
2465 produce an updated topology for subsequent NC replica
2466 syncronization between domain controllers
2468 # We may already have a samdb setup if we are
2469 # currently importing an ldif for a test run
2470 if self.samdb is None:
2472 self.load_samdb(dburl, lp, creds)
2473 except ldb.LdbError, (num, msg):
2474 logger.error("Unable to open sam database %s : %s" %
2479 if forced_local_dsa:
2480 self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" % forced_local_dsa)
2487 self.load_all_sites()
2488 self.load_all_partitions()
2489 self.load_all_transports()
2490 self.load_all_sitelinks()
2493 if opts.verify or opts.dot_files:
2495 for site in self.site_table.values():
2496 guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2497 for dnstr, dsa in site.dsa_table.items())
2499 self.plot_all_connections('dsa_initial')
2502 current_rep_table, needed_rep_table = self.my_dsa.get_rep_tables()
2503 for dnstr, c_rep in current_rep_table.items():
2504 DEBUG("c_rep %s" % c_rep)
2505 dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2507 verify_and_dot('dsa_repsFrom_initial', dot_edges, directed=True, label=self.my_dsa_dnstr,
2508 properties=(), debug=DEBUG, verify=opts.verify,
2509 dot_files=opts.dot_files)
2513 for site in self.site_table.values():
2514 for dsa in site.dsa_table.values():
2515 current_rep_table, needed_rep_table = dsa.get_rep_tables()
2516 for dn_str, rep in current_rep_table.items():
2517 for reps_from in rep.rep_repsFrom:
2518 DEBUG("rep %s" % rep)
2519 dsa_dn = guid_to_dnstr[str(reps_from.source_dsa_obj_guid)]
2520 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2522 verify_and_dot('dsa_repsFrom_initial_all', dot_edges, directed=True, label=self.my_dsa_dnstr,
2523 properties=(), debug=DEBUG, verify=opts.verify,
2524 dot_files=opts.dot_files)
2528 for link in self.sitelink_table.values():
2529 for a, b in itertools.combinations(link.site_list, 2):
2530 dot_edges.append((str(a), str(b)))
2531 verify_properties = ('connected',)
2532 verify_and_dot('dsa_sitelink_initial', dot_edges, directed=False, label=self.my_dsa_dnstr,
2533 properties=verify_properties, debug=DEBUG, verify=opts.verify,
2534 dot_files=opts.dot_files)
2537 if forget_local_links:
2538 for dsa in self.my_site.dsa_table.values():
2539 dsa.connect_table = {k:v for k, v in dsa.connect_table.items()
2540 if v.is_rodc_topology()}
2541 self.plot_all_connections('dsa_forgotten_local')
2544 if forget_intersite_links:
2545 for site in self.site_table.values():
2546 for dsa in site.dsa_table.values():
2547 dsa.connect_table = {k:v for k, v in dsa.connect_table.items()
2548 if site is self.my_site and v.is_rodc_topology()}
2550 self.plot_all_connections('dsa_forgotten_all')
2551 # These are the published steps (in order) for the
2552 # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2555 self.refresh_failed_links_connections()
2561 all_connected = self.intersite()
2564 self.remove_unneeded_ntdsconn(all_connected)
2567 self.translate_ntdsconn()
2570 self.remove_unneeded_failed_links_connections()
2573 self.update_rodc_connection()
2576 if opts.verify or opts.dot_files:
2577 self.plot_all_connections('dsa_final', ('connected', 'forest_of_rings'))
2579 DEBUG_MAGENTA("there are %d dsa guids" % len(guid_to_dnstr))
2583 my_dnstr = self.my_dsa.dsa_dnstr
2584 current_rep_table, needed_rep_table = self.my_dsa.get_rep_tables()
2585 for dnstr, n_rep in needed_rep_table.items():
2586 for reps_from in n_rep.rep_repsFrom:
2587 guid_str = str(reps_from.source_dsa_obj_guid)
2588 dot_edges.append((my_dnstr, guid_to_dnstr[guid_str]))
2589 edge_colors.append('#' + str(n_rep.nc_guid)[:6])
2591 verify_and_dot('dsa_repsFrom_final', dot_edges, directed=True, label=self.my_dsa_dnstr,
2592 properties=(), debug=DEBUG, verify=opts.verify,
2593 dot_files=opts.dot_files, edge_colors=edge_colors)
2598 for site in self.site_table.values():
2599 for dsa in site.dsa_table.values():
2600 current_rep_table, needed_rep_table = dsa.get_rep_tables()
2601 for n_rep in needed_rep_table.values():
2602 for reps_from in n_rep.rep_repsFrom:
2603 dsa_dn = guid_to_dnstr[str(reps_from.source_dsa_obj_guid)]
2604 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2606 verify_and_dot('dsa_repsFrom_final_all', dot_edges, directed=True, label=self.my_dsa_dnstr,
2607 properties=(), debug=DEBUG, verify=opts.verify,
2608 dot_files=opts.dot_files)
2616 def import_ldif(self, dburl, lp, creds, ldif_file):
2617 """Import all objects and attributes that are relevent
2618 to the KCC algorithms from a previously exported LDIF file.
2620 The point of this function is to allow a programmer/debugger to
2621 import an LDIF file with non-security relevent information that
2622 was previously extracted from a DC database. The LDIF file is used
2623 to create a temporary abbreviated database. The KCC algorithm can
2624 then run against this abbreviated database for debug or test
2625 verification that the topology generated is computationally the
2626 same between different OSes and algorithms.
2628 :param dburl: path to the temporary abbreviated db to create
2629 :param ldif_file: path to the ldif file to import
2632 self.samdb = ldif_utils.ldif_to_samdb(dburl, lp, creds, ldif_file,
2633 opts.forced_local_dsa)
2634 except ldif_utils.LdifError, e:
2639 def export_ldif(self, dburl, lp, creds, ldif_file):
2640 """Routine to extract all objects and attributes that are relevent
2641 to the KCC algorithms from a DC database.
2643 The point of this function is to allow a programmer/debugger to
2644 extract an LDIF file with non-security relevent information from
2645 a DC database. The LDIF file can then be used to "import" via
2646 the import_ldif() function this file into a temporary abbreviated
2647 database. The KCC algorithm can then run against this abbreviated
2648 database for debug or test verification that the topology generated
2649 is computationally the same between different OSes and algorithms.
2651 :param dburl: LDAP database URL to extract info from
2652 :param ldif_file: output LDIF file name to create
2655 ldif_utils.samdb_to_ldif_file(self.samdb, dburl, lp, creds, ldif_file)
2656 except ldif_utils.LdifError, e:
2661 ##################################################
2663 ##################################################
2664 def sort_replica_by_dsa_guid(rep1, rep2):
2665 return cmp(ndr_pack(rep1.rep_dsa_guid), ndr_pack(rep2.rep_dsa_guid))
2667 def sort_dsa_by_gc_and_guid(dsa1, dsa2):
2668 if dsa1.is_gc() and not dsa2.is_gc():
2670 if not dsa1.is_gc() and dsa2.is_gc():
2672 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
2674 def is_smtp_replication_available():
2675 """Currently always returns false because Samba
2676 doesn't implement SMTP transfer for NC changes
2681 def create_edge(con_type, site_link, guid_to_vertex):
2683 e.site_link = site_link
2685 for site_guid in site_link.site_list:
2686 if str(site_guid) in guid_to_vertex:
2687 e.vertices.extend(guid_to_vertex.get(str(site_guid)))
2688 e.repl_info.cost = site_link.cost
2689 e.repl_info.options = site_link.options
2690 e.repl_info.interval = site_link.interval
2691 e.repl_info.schedule = convert_schedule_to_repltimes(site_link.schedule)
2692 e.con_type = con_type
2696 def create_auto_edge_set(graph, transport):
2697 e_set = MultiEdgeSet()
2698 e_set.guid = misc.GUID() # NULL guid, not associated with a SiteLinkBridge object
2699 for site_link in graph.edges:
2700 if site_link.con_type == transport:
2701 e_set.edges.append(site_link)
2705 def create_edge_set(graph, transport, site_link_bridge):
2706 # TODO not implemented - need to store all site link bridges
2707 e_set = MultiEdgeSet()
2708 # e_set.guid = site_link_bridge
2711 def setup_vertices(graph):
2712 for v in graph.vertices:
2714 v.repl_info.cost = MAX_DWORD
2716 v.component_id = None
2718 v.repl_info.cost = 0
2722 v.repl_info.interval = 0
2723 v.repl_info.options = 0xFFFFFFFF
2724 v.repl_info.schedule = None # TODO highly suspicious
2727 def dijkstra(graph, edge_type, include_black):
2729 setup_dijkstra(graph, edge_type, include_black, queue)
2730 while len(queue) > 0:
2731 cost, guid, vertex = heapq.heappop(queue)
2732 for edge in vertex.edges:
2733 for v in edge.vertices:
2735 # add new path from vertex to v
2736 try_new_path(graph, queue, vertex, edge, v)
2738 def setup_dijkstra(graph, edge_type, include_black, queue):
2739 setup_vertices(graph)
2740 for vertex in graph.vertices:
2741 if vertex.is_white():
2744 if ((vertex.is_black() and not include_black)
2745 or edge_type not in vertex.accept_black
2746 or edge_type not in vertex.accept_red_red):
2747 vertex.repl_info.cost = MAX_DWORD
2748 vertex.root = None # NULL GUID
2749 vertex.demoted = True # Demoted appears not to be used
2751 heapq.heappush(queue, (vertex.repl_info.cost, vertex.guid, vertex))
2753 def try_new_path(graph, queue, vfrom, edge, vto):
2755 # What this function checks is that there is a valid time frame for
2756 # which replication can actually occur, despite being adequately
2758 intersect = combine_repl_info(vfrom.repl_info, edge.repl_info, newRI)
2760 # If the new path costs more than the current, then ignore the edge
2761 if newRI.cost > vto.repl_info.cost:
2764 if newRI.cost < vto.repl_info.cost and not intersect:
2767 new_duration = total_schedule(newRI.schedule)
2768 old_duration = total_schedule(vto.repl_info.schedule)
2770 # Cheaper or longer schedule
2771 if newRI.cost < vto.repl_info.cost or new_duration > old_duration:
2772 vto.root = vfrom.root
2773 vto.component_id = vfrom.component_id
2774 vto.repl_info = newRI
2775 heapq.heappush(queue, (vto.repl_info.cost, vto.guid, vto))
2777 def check_demote_vertex(vertex, edge_type):
2778 if vertex.is_white():
2781 # Accepts neither red-red nor black edges, demote
2782 if edge_type not in vertex.accept_black and edge_type not in vertex.accept_red_red:
2783 vertex.repl_info.cost = MAX_DWORD
2785 vertex.demoted = True # Demoted appears not to be used
2787 def undemote_vertex(vertex):
2788 if vertex.is_white():
2791 vertex.repl_info.cost = 0
2792 vertex.root = vertex
2793 vertex.demoted = False
2795 def process_edge_set(graph, e_set, internal_edges):
2797 for edge in graph.edges:
2798 for vertex in edge.vertices:
2799 check_demote_vertex(vertex, edge.con_type)
2800 process_edge(graph, edge, internal_edges)
2801 for vertex in edge.vertices:
2802 undemote_vertex(vertex)
2804 for edge in e_set.edges:
2805 process_edge(graph, edge, internal_edges)
2807 def process_edge(graph, examine, internal_edges):
2808 # Find the set of all vertices touches the edge to examine
2810 for v in examine.vertices:
2811 # Append a 4-tuple of color, repl cost, guid and vertex
2812 vertices.append((v.color, v.repl_info.cost, v.ndrpacked_guid, v))
2813 # Sort by color, lower
2814 DEBUG("vertices is %s" % vertices)
2817 color, cost, guid, bestv = vertices[0]
2818 # Add to internal edges an edge from every colored vertex to bestV
2819 for v in examine.vertices:
2820 if v.component_id is None or v.root is None:
2823 # Only add edge if valid inter-tree edge - needs a root and
2824 # different components
2825 if (bestv.component_id is not None and bestv.root is not None
2826 and v.component_id is not None and v.root is not None and
2827 bestv.component_id != v.component_id):
2828 add_int_edge(graph, internal_edges, examine, bestv, v)
2830 # Add internal edge, endpoints are roots of the vertices to pass in and are always colored
2831 def add_int_edge(graph, internal_edges, examine, v1, v2):
2836 if root1.is_red() and root2.is_red():
2840 if (examine.con_type not in root1.accept_red_red
2841 or examine.con_type not in root2.accept_red_red):
2844 if (examine.con_type not in root1.accept_black
2845 or examine.con_type not in root2.accept_black):
2851 # Create the transitive replInfo for the two trees and this edge
2852 if not combine_repl_info(v1.repl_info, v2.repl_info, ri):
2854 # ri is now initialized
2855 if not combine_repl_info(ri, examine.repl_info, ri2):
2858 newIntEdge = InternalEdge(root1, root2, red_red, ri2, examine.con_type, examine.site_link)
2859 # Order by vertex guid
2860 #XXX guid comparison using ndr_pack
2861 if newIntEdge.v1.ndrpacked_guid > newIntEdge.v2.ndrpacked_guid:
2862 newIntEdge.v1 = root2
2863 newIntEdge.v2 = root1
2865 internal_edges.add(newIntEdge)
2867 def kruskal(graph, edges):
2868 for v in graph.vertices:
2871 components = set([x for x in graph.vertices if not x.is_white()])
2874 # Sorted based on internal comparison function of internal edge
2877 expected_num_tree_edges = 0 # TODO this value makes little sense
2882 while index < len(edges): # TODO and num_components > 1
2884 parent1 = find_component(e.v1)
2885 parent2 = find_component(e.v2)
2886 if parent1 is not parent2:
2888 add_out_edge(graph, output_edges, e)
2889 parent1.component_id = parent2
2890 components.discard(parent1)
2894 return output_edges, len(components)
2896 def find_component(vertex):
2897 if vertex.component_id is vertex:
2901 while current.component_id is not current:
2902 current = current.component_id
2906 while current.component_id is not root:
2907 n = current.component_id
2908 current.component_id = root
2913 def add_out_edge(graph, output_edges, e):
2917 # This multi-edge is a 'real' edge with no GUID
2920 ee.site_link = e.site_link
2921 ee.vertices.append(v1)
2922 ee.vertices.append(v2)
2923 ee.con_type = e.e_type
2924 ee.repl_info = e.repl_info
2925 output_edges.append(ee)
2931 def test_all_reps_from(lp, creds):
2933 kcc.load_samdb(opts.dburl, lp, creds)
2934 dsas = kcc.list_dsas()
2939 for site in kcc.site_table.values():
2940 guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2941 for dnstr, dsa in site.dsa_table.items())
2950 kcc.run(opts.dburl, lp, creds, forced_local_dsa=dsa_dn,
2951 forget_local_links=opts.forget_local_links,
2952 forget_intersite_links=opts.forget_intersite_links)
2953 current, needed = kcc.my_dsa.get_rep_tables()
2956 for name, rep_table, rep_parts in (('needed', needed, needed_parts),
2957 ('current', current, current_parts)):
2958 for part, nc_rep in rep_table.items():
2959 edges = rep_parts.setdefault(part, [])
2960 for reps_from in nc_rep.rep_repsFrom:
2961 source = guid_to_dnstr[str(reps_from.source_dsa_obj_guid)]
2962 dest = guid_to_dnstr[str(nc_rep.rep_dsa_guid)]
2963 edges.append((source, dest))
2965 for site in kcc.site_table.values():
2966 for dsa in site.dsa_table.values():
2968 vertex_colours.append('#cc0000')
2970 vertex_colours.append('#0000cc')
2971 dot_vertices.append(dsa.dsa_dnstr)
2972 for con in dsa.connect_table.values():
2973 if con.is_rodc_topology():
2974 colours.append('red')
2976 colours.append('blue')
2977 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2979 verify_and_dot('all-dsa-connections', dot_edges, vertices=dot_vertices,
2980 label="all dsa NTDSConnections", properties=(),
2981 debug=DEBUG, verify=opts.verify, dot_files=opts.dot_files,
2982 directed=True, edge_colors=colours, vertex_colors=vertex_colours)
2984 for name, rep_parts in (('needed', needed_parts), ('current', current_parts)):
2985 for part, edges in rep_parts.items():
2986 verify_and_dot('repsFrom_%s_all_%s' % (name, part), edges, directed=True, label=part,
2987 properties=(), debug=DEBUG, verify=opts.verify,
2988 dot_files=opts.dot_files)
2992 logger = logging.getLogger("samba_kcc")
2993 logger.addHandler(logging.StreamHandler(sys.stdout))
2994 DEBUG = logger.debug
2996 def _colour_debug(*args, **kwargs):
2997 DEBUG('%s%s%s' % (kwargs['colour'], args[0], C_NORMAL), *args[1:])
2999 _globals = globals()
3000 for _colour in ('DARK_RED', 'RED', 'DARK_GREEN', 'GREEN', 'YELLOW',
3001 'DARK_YELLOW', 'DARK_BLUE', 'BLUE', 'PURPLE', 'MAGENTA',
3002 'DARK_CYAN', 'CYAN', 'GREY', 'WHITE', 'REV_RED'):
3003 _globals['DEBUG_' + _colour] = partial(_colour_debug, colour=_globals[_colour])
3006 ##################################################
3007 # samba_kcc entry point
3008 ##################################################
3010 parser = optparse.OptionParser("samba_kcc [options]")
3011 sambaopts = options.SambaOptions(parser)
3012 credopts = options.CredentialsOptions(parser)
3014 parser.add_option_group(sambaopts)
3015 parser.add_option_group(credopts)
3016 parser.add_option_group(options.VersionOptions(parser))
3018 parser.add_option("--readonly", default=False,
3019 help="compute topology but do not update database",
3020 action="store_true")
3022 parser.add_option("--debug",
3023 help="debug output",
3024 action="store_true")
3026 parser.add_option("--verify",
3027 help="verify that assorted invariants are kept",
3028 action="store_true")
3030 parser.add_option("--list-verify-tests",
3031 help="list what verification actions are available and do nothing else",
3032 action="store_true")
3034 parser.add_option("--no-dot-files", dest='dot_files',
3035 help="Don't write dot graph files in /tmp",
3036 default=True, action="store_false")
3038 parser.add_option("--seed",
3039 help="random number seed",
3042 parser.add_option("--importldif",
3043 help="import topology ldif file",
3044 type=str, metavar="<file>")
3046 parser.add_option("--exportldif",
3047 help="export topology ldif file",
3048 type=str, metavar="<file>")
3050 parser.add_option("-H", "--URL" ,
3051 help="LDB URL for database or target server",
3052 type=str, metavar="<URL>", dest="dburl")
3054 parser.add_option("--tmpdb",
3055 help="schemaless database file to create for ldif import",
3056 type=str, metavar="<file>")
3058 parser.add_option("--now",
3059 help="assume current time is this ('YYYYmmddHHMMSS[tz]', default: system time)",
3060 type=str, metavar="<date>")
3062 parser.add_option("--forced-local-dsa",
3063 help="run calculations assuming the DSA is this DN",
3064 type=str, metavar="<DSA>")
3066 parser.add_option("--attempt-live-connections", default=False,
3067 help="Attempt to connect to other DSAs to test links",
3068 action="store_true")
3070 parser.add_option("--list-valid-dsas", default=False,
3071 help="Print a list of DSA dnstrs that could be used in --forced-local-dsa",
3072 action="store_true")
3074 parser.add_option("--test-all-reps-from", default=False,
3075 help="Create and verify a graph of reps-from for every DSA",
3076 action="store_true")
3078 parser.add_option("--forget-local-links", default=False,
3079 help="pretend not to know the existing local topology",
3080 action="store_true")
3082 parser.add_option("--forget-intersite-links", default=False,
3083 help="pretend not to know the existing intersite topology",
3084 action="store_true")
3087 opts, args = parser.parse_args()
3090 if opts.list_verify_tests:
3095 logger.setLevel(logging.DEBUG)
3097 logger.setLevel(logging.INFO)
3099 logger.setLevel(logging.WARNING)
3101 # initialize seed from optional input parameter
3103 random.seed(opts.seed)
3105 random.seed(0xACE5CA11)
3108 for timeformat in ("%Y%m%d%H%M%S%Z", "%Y%m%d%H%M%S"):
3110 now_tuple = time.strptime(opts.now, timeformat)
3115 # else happens if break doesn't --> no match
3116 print >> sys.stderr, "could not parse time '%s'" % opts.now
3119 unix_now = int(time.mktime(now_tuple))
3121 unix_now = int(time.time())
3123 nt_now = unix2nttime(unix_now)
3125 lp = sambaopts.get_loadparm()
3126 creds = credopts.get_credentials(lp, fallback_machine=True)
3128 if opts.dburl is None:
3129 opts.dburl = lp.samdb_url()
3131 if opts.test_all_reps_from:
3132 opts.readonly = True
3133 test_all_reps_from(lp, creds)
3136 # Instantiate Knowledge Consistency Checker and perform run
3140 rc = kcc.export_ldif(opts.dburl, lp, creds, opts.exportldif)
3144 if opts.tmpdb is None or opts.tmpdb.startswith('ldap'):
3145 logger.error("Specify a target temp database file with --tmpdb option.")
3148 rc = kcc.import_ldif(opts.tmpdb, lp, creds, opts.importldif)
3152 if opts.list_valid_dsas:
3153 kcc.load_samdb(opts.dburl, lp, creds)
3154 print '\n'.join(kcc.list_dsas())
3158 rc = kcc.run(opts.dburl, lp, creds, opts.forced_local_dsa,
3159 opts.forget_local_links, opts.forget_intersite_links)
3162 except GraphError, e: