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 if self.my_dsa_dnstr not in self.dsa_by_dnstr:
249 DEBUG_DARK_YELLOW("my_dsa %s isn't in self.dsas_by_dnstr: it must be RODC."
250 "Let's add it, because my_dsa is a special case!\n"
251 "(likewise for self.dsa_by_guid of course)" %
254 self.dsa_by_dnstr[self.my_dsa_dnstr] = self.my_dsa
255 self.dsa_by_guid[str(self.my_dsa.dsa_guid)] = self.my_dsa
258 def load_all_partitions(self):
259 """Discover all NCs thru the Partitions dn and
260 instantiate and load the NCs.
262 Each NC is inserted into the part_table by partition
263 dn string (not the nCName dn string)
265 ::returns: Raises an Exception on error
268 res = self.samdb.search("CN=Partitions,%s" %
269 self.samdb.get_config_basedn(),
270 scope=ldb.SCOPE_SUBTREE,
271 expression="(objectClass=crossRef)")
272 except ldb.LdbError, (enum, estr):
273 raise Exception("Unable to find partitions - (%s)" % estr)
276 partstr = str(msg.dn)
279 if partstr in self.part_table:
282 part = Partition(partstr)
284 part.load_partition(self.samdb)
285 self.part_table[partstr] = part
287 def should_be_present_test(self):
288 """Enumerate all loaded partitions and DSAs in local
289 site and test if NC should be present as replica
291 for partdn, part in self.part_table.items():
292 for dsadn, dsa in self.my_site.dsa_table.items():
293 needed, ro, partial = part.should_be_present(dsa)
294 logger.info("dsadn:%s\nncdn:%s\nneeded=%s:ro=%s:partial=%s\n" %
295 (dsadn, part.nc_dnstr, needed, ro, partial))
297 def refresh_failed_links_connections(self):
298 """Based on MS-ADTS 6.2.2.1"""
300 # Instead of NULL link with failure_count = 0, the tuple is simply removed
302 # LINKS: Refresh failed links
303 self.kcc_failed_links = {}
304 current, needed = self.my_dsa.get_rep_tables()
305 for replica in current.values():
306 # For every possible connection to replicate
307 for reps_from in replica.rep_repsFrom:
308 failure_count = reps_from.consecutive_sync_failures
309 if failure_count <= 0:
312 dsa_guid = str(reps_from.source_dsa_obj_guid)
313 time_first_failure = reps_from.last_success
314 last_result = reps_from.last_attempt
315 dns_name = reps_from.dns_name1
317 f = self.kcc_failed_links.get(dsa_guid)
319 f = KCCFailedObject(dsa_guid, failure_count,
320 time_first_failure, last_result,
322 self.kcc_failed_links[dsa_guid] = f
323 #elif f.failure_count == 0:
324 # f.failure_count = failure_count
325 # f.time_first_failure = time_first_failure
326 # f.last_result = last_result
328 f.failure_count = max(f.failure_count, failure_count)
329 f.time_first_failure = min(f.time_first_failure, time_first_failure)
330 f.last_result = last_result
332 # CONNECTIONS: Refresh failed connections
333 restore_connections = set()
334 if opts.attempt_live_connections:
335 DEBUG("refresh_failed_links: checking if links are still down")
336 for connection in self.kcc_failed_connections:
338 drs_utils.drsuapi_connect(connection.dns_name, lp, creds)
339 # Failed connection is no longer failing
340 restore_connections.add(connection)
341 except drs_utils.drsException:
342 # Failed connection still failing
343 connection.failure_count += 1
345 DEBUG("refresh_failed_links: not checking live links because we weren't\n"
346 "asked to --attempt-live-connections")
348 # Remove the restored connections from the failed connections
349 self.kcc_failed_connections.difference_update(restore_connections)
351 def is_stale_link_connection(self, target_dsa):
352 """Returns False if no tuple z exists in the kCCFailedLinks or
353 kCCFailedConnections variables such that z.UUIDDsa is the
354 objectGUID of the target dsa, z.FailureCount > 0, and
355 the current time - z.TimeFirstFailure > 2 hours.
357 # Returns True if tuple z exists...
358 failed_link = self.kcc_failed_links.get(str(target_dsa.dsa_guid))
360 # failure_count should be > 0, but check anyways
361 if failed_link.failure_count > 0:
362 unix_first_time_failure = nttime2unix(failed_link.time_first_failure)
363 # TODO guard against future
364 if unix_first_time_failure > unix_now:
365 logger.error("The last success time attribute for \
366 repsFrom is in the future!")
368 # Perform calculation in seconds
369 if (unix_now - unix_first_time_failure) > 60 * 60 * 2:
376 # TODO: This should be backed by some form of local database
377 def remove_unneeded_failed_links_connections(self):
378 # Remove all tuples in kcc_failed_links where failure count = 0
379 # In this implementation, this should never happen.
381 # Remove all connections which were not used this run or connections
382 # that became active during this run.
385 def remove_unneeded_ntdsconn(self, all_connected):
386 """Removes unneeded NTDS Connections after computation
387 of KCC intra and inter-site topology has finished.
391 # Loop thru connections
392 for cn_conn in mydsa.connect_table.values():
393 if cn_conn.guid is None:
395 cn_conn.guid = misc.GUID(str(uuid.uuid4()))
396 cn_conn.whenCreated = nt_now
398 cn_conn.load_connection(self.samdb)
400 for cn_conn in mydsa.connect_table.values():
402 s_dnstr = cn_conn.get_from_dnstr()
404 cn_conn.to_be_deleted = True
407 # Get the source DSA no matter what site
408 s_dsa = self.get_dsa(s_dnstr)
410 #XXX should an RODC be regarded as same site
411 same_site = s_dnstr in self.my_site.dsa_table
413 # Given an nTDSConnection object cn, if the DC with the
414 # nTDSDSA object dc that is the parent object of cn and
415 # the DC with the nTDSDA object referenced by cn!fromServer
416 # are in the same site, the KCC on dc deletes cn if all of
417 # the following are true:
419 # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
421 # No site settings object s exists for the local DC's site, or
422 # bit NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED is clear in
425 # Another nTDSConnection object cn2 exists such that cn and
426 # cn2 have the same parent object, cn!fromServer = cn2!fromServer,
429 # cn!whenCreated < cn2!whenCreated
431 # cn!whenCreated = cn2!whenCreated and
432 # cn!objectGUID < cn2!objectGUID
434 # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
436 if not cn_conn.is_generated():
439 if self.my_site.is_cleanup_ntdsconn_disabled():
442 # Loop thru connections looking for a duplicate that
443 # fulfills the previous criteria
446 for cn2_conn in mydsa.connect_table.values():
447 if cn2_conn is cn_conn:
450 s2_dnstr = cn2_conn.get_from_dnstr()
452 # If the NTDS Connections has a different
453 # fromServer field then no match
454 if s2_dnstr != s_dnstr:
458 lesser = (cn_conn.whenCreated < cn2_conn.whenCreated or
459 (cn_conn.whenCreated == cn2_conn.whenCreated and
460 ndr_pack(cn_conn.guid) < ndr_pack(cn2_conn.guid)))
465 if lesser and not cn_conn.is_rodc_topology():
466 cn_conn.to_be_deleted = True
468 # Given an nTDSConnection object cn, if the DC with the nTDSDSA
469 # object dc that is the parent object of cn and the DC with
470 # the nTDSDSA object referenced by cn!fromServer are in
471 # different sites, a KCC acting as an ISTG in dc's site
472 # deletes cn if all of the following are true:
474 # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
476 # cn!fromServer references an nTDSDSA object for a DC
477 # in a site other than the local DC's site.
479 # The keepConnections sequence returned by
480 # CreateIntersiteConnections() does not contain
481 # cn!objectGUID, or cn is "superseded by" (see below)
482 # another nTDSConnection cn2 and keepConnections
483 # contains cn2!objectGUID.
485 # The return value of CreateIntersiteConnections()
488 # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in
491 else: # different site
493 if not mydsa.is_istg():
496 if not cn_conn.is_generated():
500 # We are directly using this connection in intersite or
501 # we are using a connection which can supersede this one.
503 # MS-ADTS 6.2.2.4 - Removing Unnecessary Connections does not
504 # appear to be correct.
506 # 1. cn!fromServer and cn!parent appear inconsistent with no cn2
507 # 2. The repsFrom do not imply each other
509 if cn_conn in self.kept_connections: # and not_superceded:
512 # This is the result of create_intersite_connections
513 if not all_connected:
516 if not cn_conn.is_rodc_topology():
517 cn_conn.to_be_deleted = True
520 if mydsa.is_ro() or opts.readonly:
521 for connect in mydsa.connect_table.values():
522 if connect.to_be_deleted:
523 DEBUG_GREEN("TO BE DELETED:\n%s" % connect)
524 if connect.to_be_added:
525 DEBUG_GREEN("TO BE ADDED:\n%s" % connect)
527 # Peform deletion from our tables but perform
528 # no database modification
529 mydsa.commit_connections(self.samdb, ro=True)
531 # Commit any modified connections
532 mydsa.commit_connections(self.samdb)
534 def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
535 """Part of MS-ADTS 6.2.2.5.
537 Update t_repsFrom if necessary to satisfy requirements. Such
538 updates are typically required when the IDL_DRSGetNCChanges
539 server has moved from one site to another--for example, to
540 enable compression when the server is moved from the
541 client's site to another site.
543 :param n_rep: NC replica we need
544 :param t_repsFrom: repsFrom tuple to modify
545 :param s_rep: NC replica at source DSA
546 :param s_dsa: source DSA
547 :param cn_conn: Local DSA NTDSConnection child
549 ::returns: (update) bit field containing which portion of the
550 repsFrom was modified. This bit field is suitable as input
551 to IDL_DRSReplicaModify ulModifyFields element, as it consists
553 drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
554 drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
555 drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
557 s_dnstr = s_dsa.dsa_dnstr
560 same_site = s_dnstr in self.my_site.dsa_table
562 # if schedule doesn't match then update and modify
563 times = convert_schedule_to_repltimes(cn_conn.schedule)
564 if times != t_repsFrom.schedule:
565 t_repsFrom.schedule = times
566 update |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
568 # Bit DRS_PER_SYNC is set in replicaFlags if and only
569 # if nTDSConnection schedule has a value v that specifies
570 # scheduled replication is to be performed at least once
572 if cn_conn.is_schedule_minimum_once_per_week():
574 if (t_repsFrom.replica_flags &
575 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0:
576 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
578 # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
579 # if the source DSA and the local DC's nTDSDSA object are
580 # in the same site or source dsa is the FSMO role owner
581 # of one or more FSMO roles in the NC replica.
582 if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
584 if (t_repsFrom.replica_flags &
585 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0:
586 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
588 # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
589 # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
590 # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
591 # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
592 # t.replicaFlags if and only if s and the local DC's
593 # nTDSDSA object are in different sites.
594 if (cn_conn.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0:
596 if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
598 if (t_repsFrom.replica_flags &
599 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0:
600 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
604 if (t_repsFrom.replica_flags &
605 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0:
606 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
608 # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
609 # and only if s and the local DC's nTDSDSA object are
610 # not in the same site and the
611 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
612 # clear in cn!options
613 if (not same_site and
615 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
617 if (t_repsFrom.replica_flags &
618 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0:
619 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
621 # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
622 # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
623 if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
625 if (t_repsFrom.replica_flags &
626 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0:
627 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
629 # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
630 # set in t.replicaFlags if and only if cn!enabledConnection = false.
631 if not cn_conn.is_enabled():
633 if (t_repsFrom.replica_flags &
634 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0:
635 t_repsFrom.replica_flags |= \
636 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
638 if (t_repsFrom.replica_flags &
639 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0:
640 t_repsFrom.replica_flags |= \
641 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
643 # If s and the local DC's nTDSDSA object are in the same site,
644 # cn!transportType has no value, or the RDN of cn!transportType
647 # Bit DRS_MAIL_REP in t.replicaFlags is clear.
649 # t.uuidTransport = NULL GUID.
651 # t.uuidDsa = The GUID-based DNS name of s.
655 # Bit DRS_MAIL_REP in t.replicaFlags is set.
657 # If x is the object with dsname cn!transportType,
658 # t.uuidTransport = x!objectGUID.
660 # Let a be the attribute identified by
661 # x!transportAddressAttribute. If a is
662 # the dNSHostName attribute, t.uuidDsa = the GUID-based
663 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
665 # It appears that the first statement i.e.
667 # "If s and the local DC's nTDSDSA object are in the same
668 # site, cn!transportType has no value, or the RDN of
669 # cn!transportType is CN=IP:"
671 # could be a slightly tighter statement if it had an "or"
672 # between each condition. I believe this should
675 # IF (same-site) OR (no-value) OR (type-ip)
677 # because IP should be the primary transport mechanism
678 # (even in inter-site) and the absense of the transportType
679 # attribute should always imply IP no matter if its multi-site
681 # NOTE MS-TECH INCORRECT:
683 # All indications point to these statements above being
684 # incorrectly stated:
686 # t.uuidDsa = The GUID-based DNS name of s.
688 # Let a be the attribute identified by
689 # x!transportAddressAttribute. If a is
690 # the dNSHostName attribute, t.uuidDsa = the GUID-based
691 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
693 # because the uuidDSA is a GUID and not a GUID-base DNS
694 # name. Nor can uuidDsa hold (s!parent)!a if not
695 # dNSHostName. What should have been said is:
697 # t.naDsa = The GUID-based DNS name of s
699 # That would also be correct if transportAddressAttribute
700 # were "mailAddress" because (naDsa) can also correctly
701 # hold the SMTP ISM service address.
703 nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
705 # We're not currently supporting SMTP replication
706 # so is_smtp_replication_available() is currently
707 # always returning False
709 cn_conn.transport_dnstr is None or
710 cn_conn.transport_dnstr.find("CN=IP") == 0 or
711 not is_smtp_replication_available()):
713 if (t_repsFrom.replica_flags &
714 drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0:
715 t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
717 t_repsFrom.transport_guid = misc.GUID()
719 # See (NOTE MS-TECH INCORRECT) above
720 if t_repsFrom.version == 0x1:
721 if t_repsFrom.dns_name1 is None or \
722 t_repsFrom.dns_name1 != nastr:
723 t_repsFrom.dns_name1 = nastr
725 if t_repsFrom.dns_name1 is None or \
726 t_repsFrom.dns_name2 is None or \
727 t_repsFrom.dns_name1 != nastr or \
728 t_repsFrom.dns_name2 != nastr:
729 t_repsFrom.dns_name1 = nastr
730 t_repsFrom.dns_name2 = nastr
733 if (t_repsFrom.replica_flags &
734 drsuapi.DRSUAPI_DRS_MAIL_REP) == 0x0:
735 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_MAIL_REP
737 # We have a transport type but its not an
738 # object in the database
739 if cn_conn.transport_guid not in self.transport_table:
740 raise Exception("Missing inter-site transport - (%s)" %
741 cn_conn.transport_dnstr)
743 x_transport = self.transport_table[str(cn_conn.transport_guid)]
745 if t_repsFrom.transport_guid != x_transport.guid:
746 t_repsFrom.transport_guid = x_transport.guid
748 # See (NOTE MS-TECH INCORRECT) above
749 if x_transport.address_attr == "dNSHostName":
751 if t_repsFrom.version == 0x1:
752 if t_repsFrom.dns_name1 is None or \
753 t_repsFrom.dns_name1 != nastr:
754 t_repsFrom.dns_name1 = nastr
756 if t_repsFrom.dns_name1 is None or \
757 t_repsFrom.dns_name2 is None or \
758 t_repsFrom.dns_name1 != nastr or \
759 t_repsFrom.dns_name2 != nastr:
760 t_repsFrom.dns_name1 = nastr
761 t_repsFrom.dns_name2 = nastr
764 # MS tech specification says we retrieve the named
765 # attribute in "transportAddressAttribute" from the parent of
768 pdnstr = s_dsa.get_parent_dnstr()
769 attrs = [ x_transport.address_attr ]
771 res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
773 except ldb.LdbError, (enum, estr):
775 "Unable to find attr (%s) for (%s) - (%s)" %
776 (x_transport.address_attr, pdnstr, estr))
779 nastr = str(msg[x_transport.address_attr][0])
781 # See (NOTE MS-TECH INCORRECT) above
782 if t_repsFrom.version == 0x1:
783 if t_repsFrom.dns_name1 is None or \
784 t_repsFrom.dns_name1 != nastr:
785 t_repsFrom.dns_name1 = nastr
787 if t_repsFrom.dns_name1 is None or \
788 t_repsFrom.dns_name2 is None or \
789 t_repsFrom.dns_name1 != nastr or \
790 t_repsFrom.dns_name2 != nastr:
792 t_repsFrom.dns_name1 = nastr
793 t_repsFrom.dns_name2 = nastr
795 if t_repsFrom.is_modified():
796 logger.debug("modify_repsFrom(): %s" % t_repsFrom)
798 def is_repsFrom_implied(self, n_rep, cn_conn):
799 """Given a NC replica and NTDS Connection, determine if the connection
800 implies a repsFrom tuple should be present from the source DSA listed
801 in the connection to the naming context
803 :param n_rep: NC replica
804 :param conn: NTDS Connection
805 ::returns (True || False), source DSA:
807 # NTDS Connection must satisfy all the following criteria
808 # to imply a repsFrom tuple is needed:
810 # cn!enabledConnection = true.
811 # cn!options does not contain NTDSCONN_OPT_RODC_TOPOLOGY.
812 # cn!fromServer references an nTDSDSA object.
815 if cn_conn.is_enabled() and not cn_conn.is_rodc_topology():
817 s_dnstr = cn_conn.get_from_dnstr()
818 if s_dnstr is not None:
819 s_dsa = self.get_dsa(s_dnstr)
821 # No DSA matching this source DN string?
825 # To imply a repsFrom tuple is needed, each of these
828 # An NC replica of the NC "is present" on the DC to
829 # which the nTDSDSA object referenced by cn!fromServer
832 # An NC replica of the NC "should be present" on
834 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
836 if s_rep is None or not s_rep.is_present():
839 # To imply a repsFrom tuple is needed, each of these
842 # The NC replica on the DC referenced by cn!fromServer is
843 # a writable replica or the NC replica that "should be
844 # present" on the local DC is a partial replica.
846 # The NC is not a domain NC, the NC replica that
847 # "should be present" on the local DC is a partial
848 # replica, cn!transportType has no value, or
849 # cn!transportType has an RDN of CN=IP.
851 implied = (not s_rep.is_ro() or n_rep.is_partial()) and \
852 (not n_rep.is_domain() or
853 n_rep.is_partial() or
854 cn_conn.transport_dnstr is None or
855 cn_conn.transport_dnstr.find("CN=IP") == 0)
862 def translate_ntdsconn(self):
863 """This function adjusts values of repsFrom abstract attributes of NC
864 replicas on the local DC to match those implied by
865 nTDSConnection objects.
868 if self.my_dsa.is_translate_ntdsconn_disabled():
869 logger.debug("skipping translate_ntdsconn() because disabling flag is set")
872 logger.debug("translate_ntdsconn(): enter")
874 current_rep_table, needed_rep_table = self.my_dsa.get_rep_tables()
876 # Filled in with replicas we currently have that need deleting
879 # We're using the MS notation names here to allow
880 # correlation back to the published algorithm.
882 # n_rep - NC replica (n)
883 # t_repsFrom - tuple (t) in n!repsFrom
884 # s_dsa - Source DSA of the replica. Defined as nTDSDSA
885 # object (s) such that (s!objectGUID = t.uuidDsa)
886 # In our IDL representation of repsFrom the (uuidDsa)
887 # attribute is called (source_dsa_obj_guid)
888 # cn_conn - (cn) is nTDSConnection object and child of the local DC's
889 # nTDSDSA object and (cn!fromServer = s)
890 # s_rep - source DSA replica of n
892 # If we have the replica and its not needed
893 # then we add it to the "to be deleted" list.
894 for dnstr in current_rep_table:
895 if dnstr not in needed_rep_table:
896 delete_reps.add(dnstr)
899 DEBUG('current %d needed %d delete %d', len(current_rep_table),
900 len(needed_rep_table), len(delete_reps))
901 DEBUG('deleting these reps: %s', delete_reps)
902 for dnstr in delete_reps:
903 del current_rep_table[dnstr]
905 # Now perform the scan of replicas we'll need
906 # and compare any current repsFrom against the
908 for n_rep in needed_rep_table.values():
910 # load any repsFrom and fsmo roles as we'll
911 # need them during connection translation
912 n_rep.load_repsFrom(self.samdb)
913 n_rep.load_fsmo_roles(self.samdb)
915 # Loop thru the existing repsFrom tupples (if any)
916 # XXX This is a list and could contain duplicates (multiple load_repsFrom calls)
917 for t_repsFrom in n_rep.rep_repsFrom:
919 # for each tuple t in n!repsFrom, let s be the nTDSDSA
920 # object such that s!objectGUID = t.uuidDsa
921 guidstr = str(t_repsFrom.source_dsa_obj_guid)
922 s_dsa = self.get_dsa_by_guidstr(guidstr)
924 # Source dsa is gone from config (strange)
925 # so cleanup stale repsFrom for unlisted DSA
927 logger.warning("repsFrom source DSA guid (%s) not found" %
929 t_repsFrom.to_be_deleted = True
932 s_dnstr = s_dsa.dsa_dnstr
934 # Retrieve my DSAs connection object (if it exists)
935 # that specifies the fromServer equivalent to
936 # the DSA that is specified in the repsFrom source
937 cn_conn = self.my_dsa.get_connection_by_from_dnstr(s_dnstr)
939 # Let (cn) be the nTDSConnection object such that (cn)
940 # is a child of the local DC's nTDSDSA object and
941 # (cn!fromServer = s) and (cn!options) does not contain
942 # NTDSCONN_OPT_RODC_TOPOLOGY or NULL if no such (cn) exists.
943 if cn_conn and not cn_conn.is_rodc_topology():
946 # KCC removes this repsFrom tuple if any of the following
950 # No NC replica of the NC "is present" on DSA that
951 # would be source of replica
953 # A writable replica of the NC "should be present" on
954 # the local DC, but a partial replica "is present" on
956 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
958 if cn_conn is None or \
959 s_rep is None or not s_rep.is_present() or \
960 (not n_rep.is_ro() and s_rep.is_partial()):
962 t_repsFrom.to_be_deleted = True
965 # If the KCC did not remove t from n!repsFrom, it updates t
966 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
968 # Loop thru connections and add implied repsFrom tuples
969 # for each NTDSConnection under our local DSA if the
970 # repsFrom is not already present
971 for cn_conn in self.my_dsa.connect_table.values():
973 implied, s_dsa = self.is_repsFrom_implied(n_rep, cn_conn)
977 # Loop thru the existing repsFrom tupples (if any) and
978 # if we already have a tuple for this connection then
979 # no need to proceed to add. It will have been changed
980 # to have the correct attributes above
981 for t_repsFrom in n_rep.rep_repsFrom:
982 guidstr = str(t_repsFrom.source_dsa_obj_guid)
983 if s_dsa is self.get_dsa_by_guidstr(guidstr):
990 # Create a new RepsFromTo and proceed to modify
991 # it according to specification
992 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
994 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
996 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
998 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
1000 # Add to our NC repsFrom as this is newly computed
1001 if t_repsFrom.is_modified():
1002 n_rep.rep_repsFrom.append(t_repsFrom)
1005 # Display any to be deleted or modified repsFrom
1006 text = n_rep.dumpstr_to_be_deleted()
1008 logger.info("TO BE DELETED:\n%s" % text)
1009 text = n_rep.dumpstr_to_be_modified()
1011 logger.info("TO BE MODIFIED:\n%s" % text)
1013 # Peform deletion from our tables but perform
1014 # no database modification
1015 n_rep.commit_repsFrom(self.samdb, ro=True)
1017 # Commit any modified repsFrom to the NC replica
1018 n_rep.commit_repsFrom(self.samdb)
1022 def merge_failed_links(self):
1023 """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
1024 The KCC on a writable DC attempts to merge the link and connection
1025 failure information from bridgehead DCs in its own site to help it
1026 identify failed bridgehead DCs.
1028 # MS-TECH Ref 6.2.2.3.2 Merge of kCCFailedLinks and kCCFailedLinks
1031 # 1. Queries every bridgehead server in your site (other than yourself)
1032 # 2. For every ntDSConnection that references a server in a different
1033 # site merge all the failure info
1035 # XXX - not implemented yet
1036 if opts.attempt_live_connections:
1037 DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1039 DEBUG("skipping merge_failed_links() because it requires real network connections\n"
1040 "and we weren't asked to --attempt-live-connctions")
1043 def setup_graph(self, part):
1044 """Set up a GRAPH, populated with a VERTEX for each site
1045 object, a MULTIEDGE for each siteLink object, and a
1046 MUTLIEDGESET for each siteLinkBridge object (or implied
1049 ::returns: a new graph
1053 g = IntersiteGraph()
1055 for site_guid, site in self.site_table.items():
1056 vertex = Vertex(site, part)
1057 vertex.guid = site_guid
1058 vertex.ndrpacked_guid = ndr_pack(site.site_guid)
1059 g.vertices.add(vertex)
1061 if not guid_to_vertex.get(site_guid):
1062 guid_to_vertex[site_guid] = []
1064 guid_to_vertex[site_guid].append(vertex)
1066 connected_vertices = set()
1067 for transport_guid, transport in self.transport_table.items():
1068 # Currently only ever "IP"
1069 for site_link_dn, site_link in self.sitelink_table.items():
1070 new_edge = create_edge(transport_guid, site_link, guid_to_vertex)
1071 connected_vertices.update(new_edge.vertices)
1072 g.edges.add(new_edge)
1074 # If 'Bridge all site links' is enabled and Win2k3 bridges required is not set
1075 # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1076 # No documentation for this however, ntdsapi.h appears to have listed:
1077 # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1078 if ((self.my_site.site_options & 0x00000002) == 0
1079 and (self.my_site.site_options & 0x00001000) == 0):
1080 g.edge_set.add(create_auto_edge_set(g, transport_guid))
1082 # TODO get all site link bridges
1083 for site_link_bridge in []:
1084 g.edge_set.add(create_edge_set(g, transport_guid,
1087 g.connected_vertices = connected_vertices
1089 #be less verbose in dot file output unless --debug
1090 do_dot_files = opts.dot_files and opts.debug
1092 for edge in g.edges:
1093 for a, b in itertools.combinations(edge.vertices, 2):
1094 dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1095 verify_properties = ()
1096 verify_and_dot('site_edges', dot_edges, directed=False, label=self.my_dsa_dnstr,
1097 properties=verify_properties, debug=DEBUG, verify=opts.verify,
1098 dot_files=do_dot_files)
1102 def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1103 """Get a bridghead DC.
1105 :param site: site object representing for which a bridgehead
1107 :param part: crossRef for NC to replicate.
1108 :param transport: interSiteTransport object for replication
1110 :param partial_ok: True if a DC containing a partial
1111 replica or a full replica will suffice, False if only
1112 a full replica will suffice.
1113 :param detect_failed: True to detect failed DCs and route
1114 replication traffic around them, False to assume no DC
1116 ::returns: dsa object for the bridgehead DC or None
1119 bhs = self.get_all_bridgeheads(site, part, transport,
1120 partial_ok, detect_failed)
1122 DEBUG_MAGENTA("get_bridgehead:\n\tsitedn=%s\n\tbhdn=None" %
1126 DEBUG_GREEN("get_bridgehead:\n\tsitedn=%s\n\tbhdn=%s" %
1127 (site.site_dnstr, bhs[0].dsa_dnstr))
1130 def get_all_bridgeheads(self, site, part, transport,
1131 partial_ok, detect_failed):
1132 """Get all bridghead DCs satisfying the given criteria
1134 :param site: site object representing the site for which
1135 bridgehead DCs are desired.
1136 :param part: partition for NC to replicate.
1137 :param transport: interSiteTransport object for
1138 replication traffic.
1139 :param partial_ok: True if a DC containing a partial
1140 replica or a full replica will suffice, False if
1141 only a full replica will suffice.
1142 :param detect_failed: True to detect failed DCs and route
1143 replication traffic around them, FALSE to assume
1145 ::returns: list of dsa object for available bridgehead
1151 logger.debug("get_all_bridgeheads: %s" % transport)
1152 if 'Site-5' in site.site_dnstr:
1153 DEBUG_RED("get_all_bridgeheads with %s, part%s, partial_ok %s"
1154 " detect_failed %s" % (site.site_dnstr, part.partstr,
1155 partial_ok, detect_failed))
1156 logger.debug(site.rw_dsa_table)
1157 for dsa in site.rw_dsa_table.values():
1159 pdnstr = dsa.get_parent_dnstr()
1161 # IF t!bridgeheadServerListBL has one or more values and
1162 # t!bridgeheadServerListBL does not contain a reference
1163 # to the parent object of dc then skip dc
1164 if (len(transport.bridgehead_list) != 0 and
1165 pdnstr not in transport.bridgehead_list):
1168 # IF dc is in the same site as the local DC
1169 # IF a replica of cr!nCName is not in the set of NC replicas
1170 # that "should be present" on dc or a partial replica of the
1171 # NC "should be present" but partialReplicasOkay = FALSE
1173 if self.my_site.same_site(dsa):
1174 needed, ro, partial = part.should_be_present(dsa)
1175 if not needed or (partial and not partial_ok):
1177 rep = dsa.get_current_replica(part.nc_dnstr)
1180 # IF an NC replica of cr!nCName is not in the set of NC
1181 # replicas that "are present" on dc or a partial replica of
1182 # the NC "is present" but partialReplicasOkay = FALSE
1185 rep = dsa.get_current_replica(part.nc_dnstr)
1186 if rep is None or (rep.is_partial() and not partial_ok):
1189 # IF AmIRODC() and cr!nCName corresponds to default NC then
1190 # Let dsaobj be the nTDSDSA object of the dc
1191 # IF dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1193 if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1194 if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1197 # IF t!name != "IP" and the parent object of dc has no value for
1198 # the attribute specified by t!transportAddressAttribute
1200 if transport.name != "IP":
1201 # MS tech specification says we retrieve the named
1202 # attribute in "transportAddressAttribute" from the parent
1205 attrs = [ transport.address_attr ]
1207 res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
1209 except ldb.LdbError, (enum, estr):
1213 if transport.address_attr not in msg:
1216 nastr = str(msg[transport.address_attr][0])
1218 # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1220 if self.is_bridgehead_failed(dsa, detect_failed):
1221 DEBUG("bridgehead is failed")
1224 logger.debug("get_all_bridgeheads: dsadn=%s" % dsa.dsa_dnstr)
1227 # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1229 # SORT bhs such that all GC servers precede DCs that are not GC
1230 # servers, and otherwise by ascending objectGUID
1232 # SORT bhs in a random order
1233 if site.is_random_bridgehead_disabled():
1234 bhs.sort(sort_dsa_by_gc_and_guid)
1241 def is_bridgehead_failed(self, dsa, detect_failed):
1242 """Determine whether a given DC is known to be in a failed state
1243 ::returns: True if and only if the DC should be considered failed
1245 Here we DEPART from the pseudo code spec which appears to be
1246 wrong. It says, in full:
1248 /***** BridgeheadDCFailed *****/
1249 /* Determine whether a given DC is known to be in a failed state.
1250 * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1251 * IN: detectFailedDCs - TRUE if and only failed DC detection is
1253 * RETURNS: TRUE if and only if the DC should be considered to be in a
1256 BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1258 IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1259 the options attribute of the site settings object for the local
1262 ELSEIF a tuple z exists in the kCCFailedLinks or
1263 kCCFailedConnections variables such that z.UUIDDsa =
1264 objectGUID, z.FailureCount > 1, and the current time -
1265 z.TimeFirstFailure > 2 hours
1268 RETURN detectFailedDCs
1272 where you will see detectFailedDCs is not behaving as
1273 advertised -- it is acting as a default return code in the
1274 event that a failure is not detected, not a switch turning
1275 detection on or off. Elsewhere the documentation seems to
1276 concur with the comment rather than the code.
1278 if not detect_failed:
1281 # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1282 # When DETECT_STALE_DISABLED, we can never know of if it's in a failed state
1283 if self.my_site.site_options & 0x00000008:
1286 return self.is_stale_link_connection(dsa)
1289 def create_connection(self, part, rbh, rsite, transport,
1290 lbh, lsite, link_opt, link_sched,
1291 partial_ok, detect_failed):
1292 """Create an nTDSConnection object with the given parameters
1293 if one does not already exist.
1295 :param part: crossRef object for the NC to replicate.
1296 :param rbh: nTDSDSA object for DC to act as the
1297 IDL_DRSGetNCChanges server (which is in a site other
1298 than the local DC's site).
1299 :param rsite: site of the rbh
1300 :param transport: interSiteTransport object for the transport
1301 to use for replication traffic.
1302 :param lbh: nTDSDSA object for DC to act as the
1303 IDL_DRSGetNCChanges client (which is in the local DC's site).
1304 :param lsite: site of the lbh
1305 :param link_opt: Replication parameters (aggregated siteLink options, etc.)
1306 :param link_sched: Schedule specifying the times at which
1307 to begin replicating.
1308 :partial_ok: True if bridgehead DCs containing partial
1309 replicas of the NC are acceptable.
1310 :param detect_failed: True to detect failed DCs and route
1311 replication traffic around them, FALSE to assume no DC
1314 rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1316 rbh_table = {x.dsa_dnstr:x for x in rbhs_all}
1318 DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all), [x.dsa_dnstr for x in rbhs_all]))
1320 # MS-TECH says to compute rbhs_avail but then doesn't use it
1321 # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1322 # partial_ok, detect_failed)
1324 lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1327 DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all), [x.dsa_dnstr for x in lbhs_all]))
1329 # MS-TECH says to compute lbhs_avail but then doesn't use it
1330 # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1331 # partial_ok, detect_failed)
1333 # FOR each nTDSConnection object cn such that the parent of cn is
1334 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1335 for ldsa in lbhs_all:
1336 for cn in ldsa.connect_table.values():
1338 rdsa = rbh_table.get(cn.from_dnstr)
1342 DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1343 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1344 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1345 # cn!transportType references t
1346 if (cn.is_generated() and not cn.is_rodc_topology() and
1347 cn.transport_guid == transport.guid):
1349 # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1350 # cn!options and cn!schedule != sch
1351 # Perform an originating update to set cn!schedule to
1353 if (not cn.is_user_owned_schedule() and
1354 not cn.is_equivalent_schedule(link_sched)):
1355 cn.schedule = link_sched
1356 cn.set_modified(True)
1358 # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1359 # NTDSCONN_OPT_USE_NOTIFY are set in cn
1360 if cn.is_override_notify_default() and \
1363 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1365 # Perform an originating update to clear bits
1366 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1367 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1368 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1370 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1371 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1372 cn.set_modified(True)
1377 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1379 # Perform an originating update to set bits
1380 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1381 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1382 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1384 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1385 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1386 cn.set_modified(True)
1389 # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1390 if cn.is_twoway_sync():
1392 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1394 # Perform an originating update to clear 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)
1403 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1405 # Perform an originating update to set bit
1406 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1407 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1408 cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1409 cn.set_modified(True)
1412 # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1414 if cn.is_intersite_compression_disabled():
1416 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1418 # Perform an originating update to clear bit
1419 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1422 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0:
1424 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1425 cn.set_modified(True)
1429 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1431 # Perform an originating update to set bit
1432 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1435 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0:
1437 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1438 cn.set_modified(True)
1440 # Display any modified connection
1442 if cn.to_be_modified:
1443 logger.info("TO BE MODIFIED:\n%s" % cn)
1445 ldsa.commit_connections(self.samdb, ro=True)
1447 ldsa.commit_connections(self.samdb)
1450 valid_connections = 0
1452 # FOR each nTDSConnection object cn such that cn!parent is
1453 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1454 for ldsa in lbhs_all:
1455 for cn in ldsa.connect_table.values():
1457 rdsa = rbh_table.get(cn.from_dnstr)
1461 DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1463 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1464 # cn!transportType references t) and
1465 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1466 if ((not cn.is_generated() or
1467 cn.transport_guid == transport.guid) and
1468 not cn.is_rodc_topology()):
1470 # LET rguid be the objectGUID of the nTDSDSA object
1471 # referenced by cn!fromServer
1472 # LET lguid be (cn!parent)!objectGUID
1474 # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1475 # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1476 # Increment cValidConnections by 1
1477 if (not self.is_bridgehead_failed(rdsa, detect_failed) and
1478 not self.is_bridgehead_failed(ldsa, detect_failed)):
1479 valid_connections += 1
1481 # IF keepConnections does not contain cn!objectGUID
1482 # APPEND cn!objectGUID to keepConnections
1483 self.kept_connections.add(cn)
1486 DEBUG_RED("valid connections %d" % valid_connections)
1487 DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1488 # IF cValidConnections = 0
1489 if valid_connections == 0:
1491 # LET opt be NTDSCONN_OPT_IS_GENERATED
1492 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1494 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1495 # SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1496 # NTDSCONN_OPT_USE_NOTIFY in opt
1497 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1498 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1499 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1501 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1502 # SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1503 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1504 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1506 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1508 # SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1510 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0:
1511 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1513 # Perform an originating update to create a new nTDSConnection
1514 # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1515 # cn!options = opt, cn!transportType is a reference to t,
1516 # cn!fromServer is a reference to rbh, and cn!schedule = sch
1517 cn = lbh.new_connection(opt, 0, transport, rbh.dsa_dnstr, link_sched)
1519 # Display any added connection
1522 logger.info("TO BE ADDED:\n%s" % cn)
1524 lbh.commit_connections(self.samdb, ro=True)
1526 lbh.commit_connections(self.samdb)
1528 # APPEND cn!objectGUID to keepConnections
1529 self.kept_connections.add(cn)
1531 def add_transports(self, vertex, local_vertex, graph, detect_failed):
1533 # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1534 # here and in the, but using vertex seems to make more
1535 # sense. That is, it wants this:
1537 #bh = self.get_bridgehead(vertex.site, vertex.part, transport,
1538 # local_vertex.is_black(), detect_failed)
1542 vertex.accept_red_red = []
1543 vertex.accept_black = []
1544 found_failed = False
1545 for t_guid, transport in self.transport_table.items():
1546 if transport.name != 'IP':
1547 #XXX well this is cheating a bit
1548 logging.warning("WARNING: we are ignoring a transport named %r" % transport.name)
1551 # FLAG_CR_NTDS_DOMAIN 0x00000002
1552 if (vertex.is_red() and transport.name != "IP" and
1553 vertex.part.system_flags & 0x00000002):
1556 if vertex not in graph.connected_vertices:
1559 partial_replica_okay = vertex.is_black()
1560 bh = self.get_bridgehead(vertex.site, vertex.part, transport,
1561 partial_replica_okay, detect_failed)
1566 vertex.accept_red_red.append(t_guid)
1567 vertex.accept_black.append(t_guid)
1569 # Add additional transport to allow another run of Dijkstra
1570 vertex.accept_red_red.append("EDGE_TYPE_ALL")
1571 vertex.accept_black.append("EDGE_TYPE_ALL")
1575 def create_connections(self, graph, part, detect_failed):
1576 """Construct an NC replica graph for the NC identified by
1577 the given crossRef, then create any additional nTDSConnection
1580 :param graph: site graph.
1581 :param part: crossRef object for NC.
1582 :param detect_failed: True to detect failed DCs and route
1583 replication traffic around them, False to assume no DC
1586 Modifies self.kept_connections by adding any connections
1587 deemed to be "in use".
1589 ::returns: (all_connected, found_failed_dc)
1590 (all_connected) True if the resulting NC replica graph
1591 connects all sites that need to be connected.
1592 (found_failed_dc) True if one or more failed DCs were
1595 all_connected = True
1596 found_failed = False
1598 logger.debug("create_connections(): enter\n\tpartdn=%s\n\tdetect_failed=%s" %
1599 (part.nc_dnstr, detect_failed))
1601 # XXX - This is a highly abbreviated function from the MS-TECH
1602 # ref. It creates connections between bridgeheads to all
1603 # sites that have appropriate replicas. Thus we are not
1604 # creating a minimum cost spanning tree but instead
1605 # producing a fully connected tree. This should produce
1606 # a full (albeit not optimal cost) replication topology.
1608 my_vertex = Vertex(self.my_site, part)
1609 my_vertex.color_vertex()
1611 for v in graph.vertices:
1613 if self.add_transports(v, my_vertex, graph, False):
1616 # No NC replicas for this NC in the site of the local DC,
1617 # so no nTDSConnection objects need be created
1618 if my_vertex.is_white():
1619 return all_connected, found_failed
1621 edge_list, component_count = self.get_spanning_tree_edges(graph, label=part.partstr)
1623 logger.debug("%s Number of components: %d" % (part.nc_dnstr, component_count))
1624 if component_count > 1:
1625 all_connected = False
1627 # LET partialReplicaOkay be TRUE if and only if
1628 # localSiteVertex.Color = COLOR.BLACK
1629 if my_vertex.is_black():
1634 # Utilize the IP transport only for now
1636 for transport in self.transport_table.values():
1637 if transport.name == "IP":
1640 if transport is None:
1641 raise Exception("Unable to find inter-site transport for IP")
1643 DEBUG("edge_list %s" % edge_list)
1645 if e.directed and e.vertices[0].site is self.my_site: # more accurate comparison?
1648 if e.vertices[0].site is self.my_site:
1649 rsite = e.vertices[1].site
1651 rsite = e.vertices[0].site
1653 # We don't make connections to our own site as that
1654 # is intrasite topology generator's job
1655 if rsite is self.my_site:
1656 DEBUG("rsite is my_site")
1659 # Determine bridgehead server in remote site
1660 rbh = self.get_bridgehead(rsite, part, transport,
1661 partial_ok, detect_failed)
1665 # RODC acts as an BH for itself
1667 # LET lbh be the nTDSDSA object of the local DC
1669 # LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1670 # cr, t, partialReplicaOkay, detectFailedDCs)
1671 if self.my_dsa.is_ro():
1672 lsite = self.my_site
1675 lsite = self.my_site
1676 lbh = self.get_bridgehead(lsite, part, transport,
1677 partial_ok, detect_failed)
1680 DEBUG_RED("DISASTER! lbh is None")
1685 DEBUG_BLUE("vertices")
1687 DEBUG_BLUE("bridgeheads")
1689 DEBUG_BLUE("-" * 70)
1691 sitelink = e.site_link
1692 if sitelink is None:
1696 link_opt = sitelink.options
1697 link_sched = sitelink.schedule
1699 self.create_connection(part, rbh, rsite, transport,
1700 lbh, lsite, link_opt, link_sched,
1701 partial_ok, detect_failed)
1703 return all_connected, found_failed
1705 def create_intersite_connections(self):
1706 """Computes an NC replica graph for each NC replica that "should be
1707 present" on the local DC or "is present" on any DC in the same site
1708 as the local DC. For each edge directed to an NC replica on such a
1709 DC from an NC replica on a DC in another site, the KCC creates an
1710 nTDSConnection object to imply that edge if one does not already
1713 Modifies self.kept_connections - A set of nTDSConnection
1714 objects for edges that are directed
1715 to the local DC's site in one or more NC replica graphs.
1717 returns: True if spanning trees were created for all NC replica
1718 graphs, otherwise False.
1720 all_connected = True
1721 self.kept_connections = set()
1723 # LET crossRefList be the set containing each object o of class
1724 # crossRef such that o is a child of the CN=Partitions child of the
1727 # FOR each crossRef object cr in crossRefList
1728 # IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1729 # is clear in cr!systemFlags, skip cr.
1730 # LET g be the GRAPH return of SetupGraph()
1732 for part in self.part_table.values():
1734 if not part.is_enabled():
1737 if part.is_foreign():
1740 graph = self.setup_graph(part)
1742 # Create nTDSConnection objects, routing replication traffic
1743 # around "failed" DCs.
1744 found_failed = False
1746 connected, found_failed = self.create_connections(graph, part, True)
1748 DEBUG("with detect_failed: connected %s Found failed %s" % (connected, found_failed))
1750 all_connected = False
1753 # One or more failed DCs preclude use of the ideal NC
1754 # replica graph. Add connections for the ideal graph.
1755 self.create_connections(graph, part, False)
1757 return all_connected
1759 def get_spanning_tree_edges(self, graph, label=None):
1760 # Phase 1: Run Dijkstra's to get a list of internal edges, which are
1761 # just the shortest-paths connecting colored vertices
1763 internal_edges = set()
1765 for e_set in graph.edge_set:
1767 for v in graph.vertices:
1770 # All con_type in an edge set is the same
1771 for e in e_set.edges:
1772 edgeType = e.con_type
1773 for v in e.vertices:
1776 if opts.verify or opts.dot_files:
1777 graph_edges = [(a.site.site_dnstr, b.site.site_dnstr)
1778 for a, b in itertools.chain(*(itertools.combinations(edge.vertices, 2)
1779 for edge in e_set.edges))]
1780 graph_nodes = [v.site.site_dnstr for v in graph.vertices]
1782 if opts.dot_files and opts.debug:
1783 write_dot_file('edgeset_%s' % (edgeType,), graph_edges, vertices=graph_nodes,
1787 verify_graph('spanning tree edge set %s' % edgeType, graph_edges, vertices=graph_nodes,
1788 properties=('complete', 'connected'), debug=DEBUG)
1790 # Run dijkstra's algorithm with just the red vertices as seeds
1791 # Seed from the full replicas
1792 dijkstra(graph, edgeType, False)
1795 process_edge_set(graph, e_set, internal_edges)
1797 # Run dijkstra's algorithm with red and black vertices as the seeds
1798 # Seed from both full and partial replicas
1799 dijkstra(graph, edgeType, True)
1802 process_edge_set(graph, e_set, internal_edges)
1804 # All vertices have root/component as itself
1805 setup_vertices(graph)
1806 process_edge_set(graph, None, internal_edges)
1808 if opts.verify or opts.dot_files:
1809 graph_edges = [(e.v1.site.site_dnstr, e.v2.site.site_dnstr) for e in internal_edges]
1810 graph_nodes = [v.site.site_dnstr for v in graph.vertices]
1811 verify_properties = ('multi_edge_forest',)
1812 verify_and_dot('prekruskal', graph_edges, graph_nodes, label=label,
1813 properties=verify_properties, debug=DEBUG, verify=opts.verify,
1814 dot_files=opts.dot_files)
1817 # Phase 2: Run Kruskal's on the internal edges
1818 output_edges, components = kruskal(graph, internal_edges)
1820 # This recalculates the cost for the path connecting the closest red vertex
1821 # Ignoring types is fine because NO suboptimal edge should exist in the graph
1822 dijkstra(graph, "EDGE_TYPE_ALL", False) # TODO rename
1823 # Phase 3: Process the output
1824 for v in graph.vertices:
1828 v.dist_to_red = v.repl_info.cost
1830 if opts.verify or opts.dot_files:
1831 graph_edges = [(e.v1.site.site_dnstr, e.v2.site.site_dnstr) for e in internal_edges]
1832 graph_nodes = [v.site.site_dnstr for v in graph.vertices]
1833 verify_properties = ('multi_edge_forest',)
1834 verify_and_dot('postkruskal', graph_edges, graph_nodes, label=label,
1835 properties=verify_properties, debug=DEBUG, verify=opts.verify,
1836 dot_files=opts.dot_files)
1838 # count the components
1839 return self.copy_output_edges(graph, output_edges), components
1841 # This ensures only one-way connections for partial-replicas
1842 def copy_output_edges(self, graph, output_edges):
1844 vid = self.my_site # object guid for the local dc's site
1846 for edge in output_edges:
1847 # Three-way edges are no problem here since these were created by
1848 # add_out_edge which only has two endpoints
1849 v = edge.vertices[0]
1850 w = edge.vertices[1]
1851 if v.site is vid or w.site is vid:
1852 if (v.is_black() or w.is_black()) and not v.dist_to_red == MAX_DWORD:
1853 edge.directed = True
1855 if w.dist_to_red < v.dist_to_red:
1856 edge.vertices[0] = w
1857 edge.vertices[1] = v
1859 edge_list.append(edge)
1863 def intersite(self):
1864 """The head method for generating the inter-site KCC replica
1865 connection graph and attendant nTDSConnection objects
1868 Produces self.kept_connections set of NTDS Connections
1869 that should be kept during subsequent pruning process.
1871 ::return (True or False): (True) if the produced NC replica
1872 graph connects all sites that need to be connected
1877 mysite = self.my_site
1878 all_connected = True
1880 logger.debug("intersite(): enter")
1882 # Determine who is the ISTG
1884 mysite.select_istg(self.samdb, mydsa, ro=True)
1886 mysite.select_istg(self.samdb, mydsa, ro=False)
1888 # Test whether local site has topology disabled
1889 if mysite.is_intersite_topology_disabled():
1890 logger.debug("intersite(): exit disabled all_connected=%d" %
1892 return all_connected
1894 if not mydsa.is_istg():
1895 logger.debug("intersite(): exit not istg all_connected=%d" %
1897 return all_connected
1899 self.merge_failed_links()
1901 # For each NC with an NC replica that "should be present" on the
1902 # local DC or "is present" on any DC in the same site as the
1903 # local DC, the KCC constructs a site graph--a precursor to an NC
1904 # replica graph. The site connectivity for a site graph is defined
1905 # by objects of class interSiteTransport, siteLink, and
1906 # siteLinkBridge in the config NC.
1908 all_connected = self.create_intersite_connections()
1910 logger.debug("intersite(): exit all_connected=%d" % all_connected)
1911 return all_connected
1913 def update_rodc_connection(self):
1914 """Runs when the local DC is an RODC and updates the RODC NTFRS
1917 # Given an nTDSConnection object cn1, such that cn1.options contains
1918 # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1919 # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1920 # that the following is true:
1922 # cn1.fromServer = cn2.fromServer
1923 # cn1.schedule = cn2.schedule
1925 # If no such cn2 can be found, cn1 is not modified.
1926 # If no such cn1 can be found, nothing is modified by this task.
1928 if not self.my_dsa.is_ro():
1931 all_connections = self.my_dsa.connect_table.values()
1932 ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1933 rw_connections = [x for x in all_connections if x not in ro_connections]
1935 # XXX here we are dealing with multiple RODC_TOPO connections,
1936 # if they exist. It is not clear whether the spec means that
1937 # or if it ever arises.
1938 if rw_connections and ro_connections:
1939 for con in ro_connections:
1940 cn2 = rw_connections[0]
1941 con.from_dnstr = cn2.from_dnstr
1942 con.schedule = cn2.schedule
1943 con.to_be_modified = True
1945 self.my_dsa.commit_connections(self.samdb, ro=opts.readonly)
1947 def intrasite_max_node_edges(self, node_count):
1948 """Returns the maximum number of edges directed to a node in
1949 the intrasite replica graph.
1951 The KCC does not create more
1952 than 50 edges directed to a single DC. To optimize replication,
1953 we compute that each node should have n+2 total edges directed
1954 to it such that (n) is the smallest non-negative integer
1955 satisfying (node_count <= 2*(n*n) + 6*n + 7)
1957 (If the number of edges is m (i.e. n + 2), that is the same as
1958 2 * m*m - 2 * m + 3).
1968 :param node_count: total number of nodes in the replica graph
1972 if node_count <= (2 * (n * n) + (6 * n) + 7):
1980 def construct_intrasite_graph(self, site_local, dc_local,
1981 nc_x, gc_only, detect_stale):
1983 # We're using the MS notation names here to allow
1984 # correlation back to the published algorithm.
1986 # nc_x - naming context (x) that we are testing if it
1987 # "should be present" on the local DC
1988 # f_of_x - replica (f) found on a DC (s) for NC (x)
1989 # dc_s - DC where f_of_x replica was found
1990 # dc_local - local DC that potentially needs a replica
1992 # r_list - replica list R
1993 # p_of_x - replica (p) is partial and found on a DC (s)
1995 # l_of_x - replica (l) is the local replica for NC (x)
1996 # that should appear on the local DC
1997 # r_len = is length of replica list |R|
1999 # If the DSA doesn't need a replica for this
2000 # partition (NC x) then continue
2001 needed, ro, partial = nc_x.should_be_present(dc_local)
2003 DEBUG_YELLOW("construct_intrasite_graph(): enter" +
2004 "\n\tgc_only=%d" % gc_only +
2005 "\n\tdetect_stale=%d" % detect_stale +
2006 "\n\tneeded=%s" % needed +
2008 "\n\tpartial=%s" % partial +
2012 DEBUG_RED("%s lacks 'should be present' status, aborting construct_intersite_graph!" %
2016 # Create a NCReplica that matches what the local replica
2017 # should say. We'll use this below in our r_list
2018 l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid,
2021 l_of_x.identify_by_basedn(self.samdb)
2023 l_of_x.rep_partial = partial
2026 # Add this replica that "should be present" to the
2027 # needed replica table for this DSA
2028 dc_local.add_needed_replica(l_of_x)
2032 # Let R be a sequence containing each writable replica f of x
2033 # such that f "is present" on a DC s satisfying the following
2036 # * s is a writable DC other than the local DC.
2038 # * s is in the same site as the local DC.
2040 # * If x is a read-only full replica and x is a domain NC,
2041 # then the DC's functional level is at least
2042 # DS_BEHAVIOR_WIN2008.
2044 # * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
2045 # in the options attribute of the site settings object for
2046 # the local DC's site, or no tuple z exists in the
2047 # kCCFailedLinks or kCCFailedConnections variables such
2048 # that z.UUIDDsa is the objectGUID of the nTDSDSA object
2049 # for s, z.FailureCount > 0, and the current time -
2050 # z.TimeFirstFailure > 2 hours.
2054 # We'll loop thru all the DSAs looking for
2055 # writeable NC replicas that match the naming
2056 # context dn for (nc_x)
2058 for dc_s in self.my_site.dsa_table.values():
2059 # If this partition (nc_x) doesn't appear as a
2060 # replica (f_of_x) on (dc_s) then continue
2061 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2064 # Pull out the NCReplica (f) of (x) with the dn
2065 # that matches NC (x) we are examining.
2066 f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2068 # Replica (f) of NC (x) must be writable
2072 # Replica (f) of NC (x) must satisfy the
2073 # "is present" criteria for DC (s) that
2075 if not f_of_x.is_present():
2078 # DC (s) must be a writable DSA other than
2079 # my local DC. In other words we'd only replicate
2080 # from other writable DC
2081 if dc_s.is_ro() or dc_s is dc_local:
2084 # Certain replica graphs are produced only
2085 # for global catalogs, so test against
2086 # method input parameter
2087 if gc_only and not dc_s.is_gc():
2090 # DC (s) must be in the same site as the local DC
2091 # as this is the intra-site algorithm. This is
2092 # handled by virtue of placing DSAs in per
2093 # site objects (see enclosing for() loop)
2095 # If NC (x) is intended to be read-only full replica
2096 # for a domain NC on the target DC then the source
2097 # DC should have functional level at minimum WIN2008
2099 # Effectively we're saying that in order to replicate
2100 # to a targeted RODC (which was introduced in Windows 2008)
2101 # then we have to replicate from a DC that is also minimally
2104 # You can also see this requirement in the MS special
2105 # considerations for RODC which state that to deploy
2106 # an RODC, at least one writable domain controller in
2107 # the domain must be running Windows Server 2008
2108 if ro and not partial and nc_x.nc_type == NCType.domain:
2109 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2112 # If we haven't been told to turn off stale connection
2113 # detection and this dsa has a stale connection then
2115 if detect_stale and self.is_stale_link_connection(dc_s):
2118 # Replica meets criteria. Add it to table indexed
2119 # by the GUID of the DC that it appears on
2120 r_list.append(f_of_x)
2122 # If a partial (not full) replica of NC (x) "should be present"
2123 # on the local DC, append to R each partial replica (p of x)
2124 # such that p "is present" on a DC satisfying the same
2125 # criteria defined above for full replica DCs.
2127 # XXX This loop and the previous one differ only in whether
2128 # the replica is partial or not. here we only accept partial
2129 # (because we're partial); before we only accepted full. Order
2130 # doen't matter (the list is sorted a few lines down) so these
2131 # loops could easily be merged. Or this could be a helper
2135 # Now we loop thru all the DSAs looking for
2136 # partial NC replicas that match the naming
2137 # context dn for (NC x)
2138 for dc_s in self.my_site.dsa_table.values():
2140 # If this partition NC (x) doesn't appear as a
2141 # replica (p) of NC (x) on the dsa DC (s) then
2143 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2146 # Pull out the NCReplica with the dn that
2147 # matches NC (x) we are examining.
2148 p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2150 # Replica (p) of NC (x) must be partial
2151 if not p_of_x.is_partial():
2154 # Replica (p) of NC (x) must satisfy the
2155 # "is present" criteria for DC (s) that
2157 if not p_of_x.is_present():
2160 # DC (s) must be a writable DSA other than
2161 # my DSA. In other words we'd only replicate
2162 # from other writable DSA
2163 if dc_s.is_ro() or dc_s is dc_local:
2166 # Certain replica graphs are produced only
2167 # for global catalogs, so test against
2168 # method input parameter
2169 if gc_only and not dc_s.is_gc():
2172 # DC (s) must be in the same site as the local DC
2173 # as this is the intra-site algorithm. This is
2174 # handled by virtue of placing DSAs in per
2175 # site objects (see enclosing for() loop)
2177 # This criteria is moot (a no-op) for this case
2178 # because we are scanning for (partial = True). The
2179 # MS algorithm statement says partial replica scans
2180 # should adhere to the "same" criteria as full replica
2181 # scans so the criteria doesn't change here...its just
2182 # rendered pointless.
2184 # The case that is occurring would be a partial domain
2185 # replica is needed on a local DC global catalog. There
2186 # is no minimum windows behavior for those since GCs
2187 # have always been present.
2188 if ro and not partial and nc_x.nc_type == NCType.domain:
2189 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2192 # If we haven't been told to turn off stale connection
2193 # detection and this dsa has a stale connection then
2195 if detect_stale and self.is_stale_link_connection(dc_s):
2198 # Replica meets criteria. Add it to table indexed
2199 # by the GUID of the DSA that it appears on
2200 r_list.append(p_of_x)
2202 # Append to R the NC replica that "should be present"
2204 r_list.append(l_of_x)
2206 r_list.sort(sort_replica_by_dsa_guid)
2209 max_node_edges = self.intrasite_max_node_edges(r_len)
2211 # Add a node for each r_list element to the replica graph
2214 node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2215 graph_list.append(node)
2217 # For each r(i) from (0 <= i < |R|-1)
2219 while i < (r_len-1):
2220 # Add an edge from r(i) to r(i+1) if r(i) is a full
2221 # replica or r(i+1) is a partial replica
2222 if not r_list[i].is_partial() or r_list[i+1].is_partial():
2223 graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
2225 # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2226 # replica or ri is a partial replica.
2227 if not r_list[i+1].is_partial() or r_list[i].is_partial():
2228 graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
2231 # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2232 # or r0 is a partial replica.
2233 if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
2234 graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
2236 # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2237 # r|R|-1 is a partial replica.
2238 if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
2239 graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
2241 DEBUG("r_list is length %s" % len(r_list))
2242 DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr)) for x in r_list))
2244 do_dot_files = opts.dot_files and opts.debug
2245 if opts.verify or do_dot_files:
2247 dot_vertices = set()
2248 for v1 in graph_list:
2249 dot_vertices.add(v1.dsa_dnstr)
2250 for v2 in v1.edge_from:
2251 dot_edges.append((v2, v1.dsa_dnstr))
2252 dot_vertices.add(v2)
2254 verify_properties = ('connected', 'directed_double_ring')
2255 verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2256 label='%s__%s__%s' % (site_local.site_dnstr, nctype_lut[nc_x.nc_type], nc_x.nc_dnstr),
2257 properties=verify_properties, debug=DEBUG, verify=opts.verify,
2258 dot_files=do_dot_files, directed=True)
2262 # For each existing nTDSConnection object implying an edge
2263 # from rj of R to ri such that j != i, an edge from rj to ri
2264 # is not already in the graph, and the total edges directed
2265 # to ri is less than n+2, the KCC adds that edge to the graph.
2266 for vertex in graph_list:
2267 dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2268 for connect in dsa.connect_table.values():
2269 remote = connect.from_dnstr
2270 if remote in self.my_site.dsa_table:
2271 vertex.add_edge_from(remote)
2273 DEBUG('reps are: %s' % ' '.join(x.rep_dsa_dnstr for x in r_list))
2274 DEBUG('dsas are: %s' % ' '.join(x.dsa_dnstr for x in graph_list))
2276 for tnode in graph_list:
2277 # To optimize replication latency in sites with many NC replicas, the
2278 # KCC adds new edges directed to ri to bring the total edges to n+2,
2279 # where the NC replica rk of R from which the edge is directed
2280 # is chosen at random such that k != i and an edge from rk to ri
2281 # is not already in the graph.
2283 # Note that the KCC tech ref does not give a number for the definition
2284 # of "sites with many NC replicas". At a bare minimum to satisfy
2285 # n+2 edges directed at a node we have to have at least three replicas
2286 # in |R| (i.e. if n is zero then at least replicas from two other graph
2287 # nodes may direct edges to us).
2288 if r_len >= 3 and not tnode.has_sufficient_edges():
2289 candidates = [x for x in graph_list if (x is not tnode and
2290 x.dsa_dnstr not in tnode.edge_from)]
2292 DEBUG_BLUE("looking for random link for %s. r_len %d, graph len %d candidates %d"
2293 % (tnode.dsa_dnstr, r_len, len(graph_list), len(candidates)))
2295 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2297 while candidates and not tnode.has_sufficient_edges():
2298 other = random.choice(candidates)
2299 DEBUG("trying to add candidate %s" % other.dsa_dstr)
2300 if not tnode.add_edge_from(other):
2301 DEBUG_RED("could not add %s" % other.dsa_dstr)
2302 candidates.remove(other)
2304 DEBUG_CYAN("not adding links to %s: nodes %s, links is %s/%s" %
2305 (tnode.dsa_dnstr, r_len, len(tnode.edge_from), tnode.max_edges))
2308 # Print the graph node in debug mode
2309 logger.debug("%s" % tnode)
2311 # For each edge directed to the local DC, ensure a nTDSConnection
2312 # points to us that satisfies the KCC criteria
2314 if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2315 tnode.add_connections_from_edges(dc_local)
2318 if opts.verify or do_dot_files:
2320 dot_vertices = set()
2321 for v1 in graph_list:
2322 dot_vertices.add(v1.dsa_dnstr)
2323 for v2 in v1.edge_from:
2324 dot_edges.append((v2, v1.dsa_dnstr))
2325 dot_vertices.add(v2)
2327 verify_properties = ('connected', 'directed_double_ring_or_small')
2328 verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2329 label='%s__%s__%s' % (site_local.site_dnstr, nctype_lut[nc_x.nc_type], nc_x.nc_dnstr),
2330 properties=verify_properties, debug=DEBUG, verify=opts.verify,
2331 dot_files=do_dot_files, directed=True)
2334 def intrasite(self):
2335 """The head method for generating the intra-site KCC replica
2336 connection graph and attendant nTDSConnection objects
2342 logger.debug("intrasite(): enter")
2344 # Test whether local site has topology disabled
2345 mysite = self.my_site
2346 if mysite.is_intrasite_topology_disabled():
2349 detect_stale = (not mysite.is_detect_stale_disabled())
2350 for connect in mydsa.connect_table.values():
2351 if connect.to_be_added:
2352 DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2354 # Loop thru all the partitions, with gc_only False
2355 for partdn, part in self.part_table.items():
2356 self.construct_intrasite_graph(mysite, mydsa, part, False,
2358 for connect in mydsa.connect_table.values():
2359 if connect.to_be_added:
2360 DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2363 # If the DC is a GC server, the KCC constructs an additional NC
2364 # replica graph (and creates nTDSConnection objects) for the
2365 # config NC as above, except that only NC replicas that "are present"
2366 # on GC servers are added to R.
2367 for connect in mydsa.connect_table.values():
2368 if connect.to_be_added:
2369 DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2371 # Do it again, with gc_only True
2372 for partdn, part in self.part_table.items():
2373 if part.is_config():
2374 self.construct_intrasite_graph(mysite, mydsa, part, True,
2377 # The DC repeats the NC replica graph computation and nTDSConnection
2378 # creation for each of the NC replica graphs, this time assuming
2379 # that no DC has failed. It does so by re-executing the steps as
2380 # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2381 # set in the options attribute of the site settings object for
2382 # the local DC's site. (ie. we set "detec_stale" flag to False)
2383 for connect in mydsa.connect_table.values():
2384 if connect.to_be_added:
2385 DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2387 # Loop thru all the partitions.
2388 for partdn, part in self.part_table.items():
2389 self.construct_intrasite_graph(mysite, mydsa, part, False,
2390 False) # don't detect stale
2392 # If the DC is a GC server, the KCC constructs an additional NC
2393 # replica graph (and creates nTDSConnection objects) for the
2394 # config NC as above, except that only NC replicas that "are present"
2395 # on GC servers are added to R.
2396 for connect in mydsa.connect_table.values():
2397 if connect.to_be_added:
2398 DEBUG_RED("TO BE ADDED:\n%s" % connect)
2400 for partdn, part in self.part_table.items():
2401 if part.is_config():
2402 self.construct_intrasite_graph(mysite, mydsa, part, True,
2403 False) # don't detect stale
2406 # Display any to be added or modified repsFrom
2407 for connect in mydsa.connect_table.values():
2408 if connect.to_be_deleted:
2409 logger.info("TO BE DELETED:\n%s" % connect)
2410 if connect.to_be_modified:
2411 logger.info("TO BE MODIFIED:\n%s" % connect)
2412 if connect.to_be_added:
2413 DEBUG_GREEN("TO BE ADDED:\n%s" % connect)
2415 mydsa.commit_connections(self.samdb, ro=True)
2417 # Commit any newly created connections to the samdb
2418 mydsa.commit_connections(self.samdb)
2421 def list_dsas(self):
2425 self.load_all_sites()
2426 self.load_all_partitions()
2427 self.load_all_transports()
2428 self.load_all_sitelinks()
2430 for site in self.site_table.values():
2431 dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2432 for dsa in site.dsa_table.values()])
2435 def load_samdb(self, dburl, lp, creds):
2436 self.samdb = SamDB(url=dburl,
2437 session_info=system_session(),
2438 credentials=creds, lp=lp)
2442 def plot_all_connections(self, basename, verify_properties=()):
2443 verify = verify_properties and opts.verify
2444 plot = opts.dot_files
2445 if not (verify or plot):
2453 for dsa in self.dsa_by_dnstr.values():
2454 dot_vertices.append(dsa.dsa_dnstr)
2456 vertex_colours.append('#cc0000')
2458 vertex_colours.append('#0000cc')
2459 for con in dsa.connect_table.values():
2460 if con.is_rodc_topology():
2461 edge_colours.append('red')
2463 edge_colours.append('blue')
2464 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2466 verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2467 label=self.my_dsa_dnstr, properties=verify_properties,
2468 debug=DEBUG, verify=verify, dot_files=plot,
2469 directed=True, edge_colors=edge_colours,
2470 vertex_colors=vertex_colours)
2473 def run(self, dburl, lp, creds, forced_local_dsa=None,
2474 forget_local_links=False, forget_intersite_links=False):
2475 """Method to perform a complete run of the KCC and
2476 produce an updated topology for subsequent NC replica
2477 syncronization between domain controllers
2479 # We may already have a samdb setup if we are
2480 # currently importing an ldif for a test run
2481 if self.samdb is None:
2483 self.load_samdb(dburl, lp, creds)
2484 except ldb.LdbError, (num, msg):
2485 logger.error("Unable to open sam database %s : %s" %
2490 if forced_local_dsa:
2491 self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" % forced_local_dsa)
2498 self.load_all_sites()
2499 self.load_all_partitions()
2500 self.load_all_transports()
2501 self.load_all_sitelinks()
2504 if opts.verify or opts.dot_files:
2506 for site in self.site_table.values():
2507 guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2508 for dnstr, dsa in site.dsa_table.items())
2510 self.plot_all_connections('dsa_initial')
2513 current_rep_table, needed_rep_table = self.my_dsa.get_rep_tables()
2514 for dnstr, c_rep in current_rep_table.items():
2515 DEBUG("c_rep %s" % c_rep)
2516 dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2518 verify_and_dot('dsa_repsFrom_initial', dot_edges, directed=True, label=self.my_dsa_dnstr,
2519 properties=(), debug=DEBUG, verify=opts.verify,
2520 dot_files=opts.dot_files)
2524 for site in self.site_table.values():
2525 for dsa in site.dsa_table.values():
2526 current_rep_table, needed_rep_table = dsa.get_rep_tables()
2527 for dn_str, rep in current_rep_table.items():
2528 for reps_from in rep.rep_repsFrom:
2529 DEBUG("rep %s" % rep)
2530 dsa_dn = guid_to_dnstr[str(reps_from.source_dsa_obj_guid)]
2531 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2533 verify_and_dot('dsa_repsFrom_initial_all', dot_edges, directed=True, label=self.my_dsa_dnstr,
2534 properties=(), debug=DEBUG, verify=opts.verify,
2535 dot_files=opts.dot_files)
2539 for link in self.sitelink_table.values():
2540 for a, b in itertools.combinations(link.site_list, 2):
2541 dot_edges.append((str(a), str(b)))
2542 verify_properties = ('connected',)
2543 verify_and_dot('dsa_sitelink_initial', dot_edges, directed=False, label=self.my_dsa_dnstr,
2544 properties=verify_properties, debug=DEBUG, verify=opts.verify,
2545 dot_files=opts.dot_files)
2548 if forget_local_links:
2549 for dsa in self.my_site.dsa_table.values():
2550 dsa.connect_table = {k:v for k, v in dsa.connect_table.items()
2551 if v.is_rodc_topology()}
2552 self.plot_all_connections('dsa_forgotten_local')
2555 if forget_intersite_links:
2556 for site in self.site_table.values():
2557 for dsa in site.dsa_table.values():
2558 dsa.connect_table = {k:v for k, v in dsa.connect_table.items()
2559 if site is self.my_site and v.is_rodc_topology()}
2561 self.plot_all_connections('dsa_forgotten_all')
2562 # These are the published steps (in order) for the
2563 # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2566 self.refresh_failed_links_connections()
2572 all_connected = self.intersite()
2575 self.remove_unneeded_ntdsconn(all_connected)
2578 self.translate_ntdsconn()
2581 self.remove_unneeded_failed_links_connections()
2584 self.update_rodc_connection()
2587 if opts.verify or opts.dot_files:
2588 self.plot_all_connections('dsa_final', ('connected', 'forest_of_rings'))
2590 DEBUG_MAGENTA("there are %d dsa guids" % len(guid_to_dnstr))
2594 my_dnstr = self.my_dsa.dsa_dnstr
2595 current_rep_table, needed_rep_table = self.my_dsa.get_rep_tables()
2596 for dnstr, n_rep in needed_rep_table.items():
2597 for reps_from in n_rep.rep_repsFrom:
2598 guid_str = str(reps_from.source_dsa_obj_guid)
2599 dot_edges.append((my_dnstr, guid_to_dnstr[guid_str]))
2600 edge_colors.append('#' + str(n_rep.nc_guid)[:6])
2602 verify_and_dot('dsa_repsFrom_final', dot_edges, directed=True, label=self.my_dsa_dnstr,
2603 properties=(), debug=DEBUG, verify=opts.verify,
2604 dot_files=opts.dot_files, edge_colors=edge_colors)
2609 for site in self.site_table.values():
2610 for dsa in site.dsa_table.values():
2611 current_rep_table, needed_rep_table = dsa.get_rep_tables()
2612 for n_rep in needed_rep_table.values():
2613 for reps_from in n_rep.rep_repsFrom:
2614 dsa_dn = guid_to_dnstr[str(reps_from.source_dsa_obj_guid)]
2615 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2617 verify_and_dot('dsa_repsFrom_final_all', dot_edges, directed=True, label=self.my_dsa_dnstr,
2618 properties=(), debug=DEBUG, verify=opts.verify,
2619 dot_files=opts.dot_files)
2627 def import_ldif(self, dburl, lp, creds, ldif_file):
2628 """Import all objects and attributes that are relevent
2629 to the KCC algorithms from a previously exported LDIF file.
2631 The point of this function is to allow a programmer/debugger to
2632 import an LDIF file with non-security relevent information that
2633 was previously extracted from a DC database. The LDIF file is used
2634 to create a temporary abbreviated database. The KCC algorithm can
2635 then run against this abbreviated database for debug or test
2636 verification that the topology generated is computationally the
2637 same between different OSes and algorithms.
2639 :param dburl: path to the temporary abbreviated db to create
2640 :param ldif_file: path to the ldif file to import
2643 self.samdb = ldif_utils.ldif_to_samdb(dburl, lp, creds, ldif_file,
2644 opts.forced_local_dsa)
2645 except ldif_utils.LdifError, e:
2650 def export_ldif(self, dburl, lp, creds, ldif_file):
2651 """Routine to extract all objects and attributes that are relevent
2652 to the KCC algorithms from a DC database.
2654 The point of this function is to allow a programmer/debugger to
2655 extract an LDIF file with non-security relevent information from
2656 a DC database. The LDIF file can then be used to "import" via
2657 the import_ldif() function this file into a temporary abbreviated
2658 database. The KCC algorithm can then run against this abbreviated
2659 database for debug or test verification that the topology generated
2660 is computationally the same between different OSes and algorithms.
2662 :param dburl: LDAP database URL to extract info from
2663 :param ldif_file: output LDIF file name to create
2666 ldif_utils.samdb_to_ldif_file(self.samdb, dburl, lp, creds, ldif_file)
2667 except ldif_utils.LdifError, e:
2672 ##################################################
2674 ##################################################
2675 def sort_replica_by_dsa_guid(rep1, rep2):
2676 return cmp(ndr_pack(rep1.rep_dsa_guid), ndr_pack(rep2.rep_dsa_guid))
2678 def sort_dsa_by_gc_and_guid(dsa1, dsa2):
2679 if dsa1.is_gc() and not dsa2.is_gc():
2681 if not dsa1.is_gc() and dsa2.is_gc():
2683 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
2685 def is_smtp_replication_available():
2686 """Currently always returns false because Samba
2687 doesn't implement SMTP transfer for NC changes
2692 def create_edge(con_type, site_link, guid_to_vertex):
2694 e.site_link = site_link
2696 for site_guid in site_link.site_list:
2697 if str(site_guid) in guid_to_vertex:
2698 e.vertices.extend(guid_to_vertex.get(str(site_guid)))
2699 e.repl_info.cost = site_link.cost
2700 e.repl_info.options = site_link.options
2701 e.repl_info.interval = site_link.interval
2702 e.repl_info.schedule = convert_schedule_to_repltimes(site_link.schedule)
2703 e.con_type = con_type
2707 def create_auto_edge_set(graph, transport):
2708 e_set = MultiEdgeSet()
2709 e_set.guid = misc.GUID() # NULL guid, not associated with a SiteLinkBridge object
2710 for site_link in graph.edges:
2711 if site_link.con_type == transport:
2712 e_set.edges.append(site_link)
2716 def create_edge_set(graph, transport, site_link_bridge):
2717 # TODO not implemented - need to store all site link bridges
2718 e_set = MultiEdgeSet()
2719 # e_set.guid = site_link_bridge
2722 def setup_vertices(graph):
2723 for v in graph.vertices:
2725 v.repl_info.cost = MAX_DWORD
2727 v.component_id = None
2729 v.repl_info.cost = 0
2733 v.repl_info.interval = 0
2734 v.repl_info.options = 0xFFFFFFFF
2735 v.repl_info.schedule = None # TODO highly suspicious
2738 def dijkstra(graph, edge_type, include_black):
2740 setup_dijkstra(graph, edge_type, include_black, queue)
2741 while len(queue) > 0:
2742 cost, guid, vertex = heapq.heappop(queue)
2743 for edge in vertex.edges:
2744 for v in edge.vertices:
2746 # add new path from vertex to v
2747 try_new_path(graph, queue, vertex, edge, v)
2749 def setup_dijkstra(graph, edge_type, include_black, queue):
2750 setup_vertices(graph)
2751 for vertex in graph.vertices:
2752 if vertex.is_white():
2755 if ((vertex.is_black() and not include_black)
2756 or edge_type not in vertex.accept_black
2757 or edge_type not in vertex.accept_red_red):
2758 vertex.repl_info.cost = MAX_DWORD
2759 vertex.root = None # NULL GUID
2760 vertex.demoted = True # Demoted appears not to be used
2762 heapq.heappush(queue, (vertex.repl_info.cost, vertex.guid, vertex))
2764 def try_new_path(graph, queue, vfrom, edge, vto):
2766 # What this function checks is that there is a valid time frame for
2767 # which replication can actually occur, despite being adequately
2769 intersect = combine_repl_info(vfrom.repl_info, edge.repl_info, newRI)
2771 # If the new path costs more than the current, then ignore the edge
2772 if newRI.cost > vto.repl_info.cost:
2775 if newRI.cost < vto.repl_info.cost and not intersect:
2778 new_duration = total_schedule(newRI.schedule)
2779 old_duration = total_schedule(vto.repl_info.schedule)
2781 # Cheaper or longer schedule
2782 if newRI.cost < vto.repl_info.cost or new_duration > old_duration:
2783 vto.root = vfrom.root
2784 vto.component_id = vfrom.component_id
2785 vto.repl_info = newRI
2786 heapq.heappush(queue, (vto.repl_info.cost, vto.guid, vto))
2788 def check_demote_vertex(vertex, edge_type):
2789 if vertex.is_white():
2792 # Accepts neither red-red nor black edges, demote
2793 if edge_type not in vertex.accept_black and edge_type not in vertex.accept_red_red:
2794 vertex.repl_info.cost = MAX_DWORD
2796 vertex.demoted = True # Demoted appears not to be used
2798 def undemote_vertex(vertex):
2799 if vertex.is_white():
2802 vertex.repl_info.cost = 0
2803 vertex.root = vertex
2804 vertex.demoted = False
2806 def process_edge_set(graph, e_set, internal_edges):
2808 for edge in graph.edges:
2809 for vertex in edge.vertices:
2810 check_demote_vertex(vertex, edge.con_type)
2811 process_edge(graph, edge, internal_edges)
2812 for vertex in edge.vertices:
2813 undemote_vertex(vertex)
2815 for edge in e_set.edges:
2816 process_edge(graph, edge, internal_edges)
2818 def process_edge(graph, examine, internal_edges):
2819 # Find the set of all vertices touches the edge to examine
2821 for v in examine.vertices:
2822 # Append a 4-tuple of color, repl cost, guid and vertex
2823 vertices.append((v.color, v.repl_info.cost, v.ndrpacked_guid, v))
2824 # Sort by color, lower
2825 DEBUG("vertices is %s" % vertices)
2828 color, cost, guid, bestv = vertices[0]
2829 # Add to internal edges an edge from every colored vertex to bestV
2830 for v in examine.vertices:
2831 if v.component_id is None or v.root is None:
2834 # Only add edge if valid inter-tree edge - needs a root and
2835 # different components
2836 if (bestv.component_id is not None and bestv.root is not None
2837 and v.component_id is not None and v.root is not None and
2838 bestv.component_id != v.component_id):
2839 add_int_edge(graph, internal_edges, examine, bestv, v)
2841 # Add internal edge, endpoints are roots of the vertices to pass in and are always colored
2842 def add_int_edge(graph, internal_edges, examine, v1, v2):
2847 if root1.is_red() and root2.is_red():
2851 if (examine.con_type not in root1.accept_red_red
2852 or examine.con_type not in root2.accept_red_red):
2855 if (examine.con_type not in root1.accept_black
2856 or examine.con_type not in root2.accept_black):
2862 # Create the transitive replInfo for the two trees and this edge
2863 if not combine_repl_info(v1.repl_info, v2.repl_info, ri):
2865 # ri is now initialized
2866 if not combine_repl_info(ri, examine.repl_info, ri2):
2869 newIntEdge = InternalEdge(root1, root2, red_red, ri2, examine.con_type, examine.site_link)
2870 # Order by vertex guid
2871 #XXX guid comparison using ndr_pack
2872 if newIntEdge.v1.ndrpacked_guid > newIntEdge.v2.ndrpacked_guid:
2873 newIntEdge.v1 = root2
2874 newIntEdge.v2 = root1
2876 internal_edges.add(newIntEdge)
2878 def kruskal(graph, edges):
2879 for v in graph.vertices:
2882 components = set([x for x in graph.vertices if not x.is_white()])
2885 # Sorted based on internal comparison function of internal edge
2888 expected_num_tree_edges = 0 # TODO this value makes little sense
2893 while index < len(edges): # TODO and num_components > 1
2895 parent1 = find_component(e.v1)
2896 parent2 = find_component(e.v2)
2897 if parent1 is not parent2:
2899 add_out_edge(graph, output_edges, e)
2900 parent1.component_id = parent2
2901 components.discard(parent1)
2905 return output_edges, len(components)
2907 def find_component(vertex):
2908 if vertex.component_id is vertex:
2912 while current.component_id is not current:
2913 current = current.component_id
2917 while current.component_id is not root:
2918 n = current.component_id
2919 current.component_id = root
2924 def add_out_edge(graph, output_edges, e):
2928 # This multi-edge is a 'real' edge with no GUID
2931 ee.site_link = e.site_link
2932 ee.vertices.append(v1)
2933 ee.vertices.append(v2)
2934 ee.con_type = e.e_type
2935 ee.repl_info = e.repl_info
2936 output_edges.append(ee)
2942 def test_all_reps_from(lp, creds):
2944 kcc.load_samdb(opts.dburl, lp, creds)
2945 dsas = kcc.list_dsas()
2950 for site in kcc.site_table.values():
2951 guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2952 for dnstr, dsa in site.dsa_table.items())
2961 kcc.run(opts.dburl, lp, creds, forced_local_dsa=dsa_dn,
2962 forget_local_links=opts.forget_local_links,
2963 forget_intersite_links=opts.forget_intersite_links)
2964 current, needed = kcc.my_dsa.get_rep_tables()
2967 for name, rep_table, rep_parts in (('needed', needed, needed_parts),
2968 ('current', current, current_parts)):
2969 for part, nc_rep in rep_table.items():
2970 edges = rep_parts.setdefault(part, [])
2971 for reps_from in nc_rep.rep_repsFrom:
2972 source = guid_to_dnstr[str(reps_from.source_dsa_obj_guid)]
2973 dest = guid_to_dnstr[str(nc_rep.rep_dsa_guid)]
2974 edges.append((source, dest))
2976 for site in kcc.site_table.values():
2977 for dsa in site.dsa_table.values():
2979 vertex_colours.append('#cc0000')
2981 vertex_colours.append('#0000cc')
2982 dot_vertices.append(dsa.dsa_dnstr)
2983 for con in dsa.connect_table.values():
2984 if con.is_rodc_topology():
2985 colours.append('red')
2987 colours.append('blue')
2988 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2990 verify_and_dot('all-dsa-connections', dot_edges, vertices=dot_vertices,
2991 label="all dsa NTDSConnections", properties=(),
2992 debug=DEBUG, verify=opts.verify, dot_files=opts.dot_files,
2993 directed=True, edge_colors=colours, vertex_colors=vertex_colours)
2995 for name, rep_parts in (('needed', needed_parts), ('current', current_parts)):
2996 for part, edges in rep_parts.items():
2997 verify_and_dot('repsFrom_%s_all_%s' % (name, part), edges, directed=True, label=part,
2998 properties=(), debug=DEBUG, verify=opts.verify,
2999 dot_files=opts.dot_files)
3003 logger = logging.getLogger("samba_kcc")
3004 logger.addHandler(logging.StreamHandler(sys.stdout))
3005 DEBUG = logger.debug
3007 def _colour_debug(*args, **kwargs):
3008 DEBUG('%s%s%s' % (kwargs['colour'], args[0], C_NORMAL), *args[1:])
3010 _globals = globals()
3011 for _colour in ('DARK_RED', 'RED', 'DARK_GREEN', 'GREEN', 'YELLOW',
3012 'DARK_YELLOW', 'DARK_BLUE', 'BLUE', 'PURPLE', 'MAGENTA',
3013 'DARK_CYAN', 'CYAN', 'GREY', 'WHITE', 'REV_RED'):
3014 _globals['DEBUG_' + _colour] = partial(_colour_debug, colour=_globals[_colour])
3017 ##################################################
3018 # samba_kcc entry point
3019 ##################################################
3021 parser = optparse.OptionParser("samba_kcc [options]")
3022 sambaopts = options.SambaOptions(parser)
3023 credopts = options.CredentialsOptions(parser)
3025 parser.add_option_group(sambaopts)
3026 parser.add_option_group(credopts)
3027 parser.add_option_group(options.VersionOptions(parser))
3029 parser.add_option("--readonly", default=False,
3030 help="compute topology but do not update database",
3031 action="store_true")
3033 parser.add_option("--debug",
3034 help="debug output",
3035 action="store_true")
3037 parser.add_option("--verify",
3038 help="verify that assorted invariants are kept",
3039 action="store_true")
3041 parser.add_option("--list-verify-tests",
3042 help="list what verification actions are available and do nothing else",
3043 action="store_true")
3045 parser.add_option("--no-dot-files", dest='dot_files',
3046 help="Don't write dot graph files in /tmp",
3047 default=True, action="store_false")
3049 parser.add_option("--seed",
3050 help="random number seed",
3053 parser.add_option("--importldif",
3054 help="import topology ldif file",
3055 type=str, metavar="<file>")
3057 parser.add_option("--exportldif",
3058 help="export topology ldif file",
3059 type=str, metavar="<file>")
3061 parser.add_option("-H", "--URL" ,
3062 help="LDB URL for database or target server",
3063 type=str, metavar="<URL>", dest="dburl")
3065 parser.add_option("--tmpdb",
3066 help="schemaless database file to create for ldif import",
3067 type=str, metavar="<file>")
3069 parser.add_option("--now",
3070 help="assume current time is this ('YYYYmmddHHMMSS[tz]', default: system time)",
3071 type=str, metavar="<date>")
3073 parser.add_option("--forced-local-dsa",
3074 help="run calculations assuming the DSA is this DN",
3075 type=str, metavar="<DSA>")
3077 parser.add_option("--attempt-live-connections", default=False,
3078 help="Attempt to connect to other DSAs to test links",
3079 action="store_true")
3081 parser.add_option("--list-valid-dsas", default=False,
3082 help="Print a list of DSA dnstrs that could be used in --forced-local-dsa",
3083 action="store_true")
3085 parser.add_option("--test-all-reps-from", default=False,
3086 help="Create and verify a graph of reps-from for every DSA",
3087 action="store_true")
3089 parser.add_option("--forget-local-links", default=False,
3090 help="pretend not to know the existing local topology",
3091 action="store_true")
3093 parser.add_option("--forget-intersite-links", default=False,
3094 help="pretend not to know the existing intersite topology",
3095 action="store_true")
3098 opts, args = parser.parse_args()
3101 if opts.list_verify_tests:
3106 logger.setLevel(logging.DEBUG)
3108 logger.setLevel(logging.INFO)
3110 logger.setLevel(logging.WARNING)
3112 # initialize seed from optional input parameter
3114 random.seed(opts.seed)
3116 random.seed(0xACE5CA11)
3119 for timeformat in ("%Y%m%d%H%M%S%Z", "%Y%m%d%H%M%S"):
3121 now_tuple = time.strptime(opts.now, timeformat)
3126 # else happens if break doesn't --> no match
3127 print >> sys.stderr, "could not parse time '%s'" % opts.now
3130 unix_now = int(time.mktime(now_tuple))
3132 unix_now = int(time.time())
3134 nt_now = unix2nttime(unix_now)
3136 lp = sambaopts.get_loadparm()
3137 creds = credopts.get_credentials(lp, fallback_machine=True)
3139 if opts.dburl is None:
3140 opts.dburl = lp.samdb_url()
3142 if opts.test_all_reps_from:
3143 opts.readonly = True
3144 test_all_reps_from(lp, creds)
3147 # Instantiate Knowledge Consistency Checker and perform run
3151 rc = kcc.export_ldif(opts.dburl, lp, creds, opts.exportldif)
3155 if opts.tmpdb is None or opts.tmpdb.startswith('ldap'):
3156 logger.error("Specify a target temp database file with --tmpdb option.")
3159 rc = kcc.import_ldif(opts.tmpdb, lp, creds, opts.importldif)
3163 if opts.list_valid_dsas:
3164 kcc.load_samdb(opts.dburl, lp, creds)
3165 print '\n'.join(kcc.list_dsas())
3169 rc = kcc.run(opts.dburl, lp, creds, opts.forced_local_dsa,
3170 opts.forget_local_links, opts.forget_intersite_links)
3173 except GraphError, e: