kcc: correct a typo in the debug messages
[Samba.git] / python / samba / kcc / __init__.py
blobe96ff836437a840e1bb85160c03813073d3cbc3e
1 # define the KCC object
3 # Copyright (C) Dave Craft 2011
4 # Copyright (C) Andrew Bartlett 2015
6 # Andrew Bartlett's alleged work performed by his underlings Douglas
7 # Bagnall and Garming Sam.
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 import random
23 import uuid
25 import itertools
26 from samba import unix2nttime, nttime2unix
27 from samba import ldb, dsdb, drs_utils
28 from samba.auth import system_session
29 from samba.samdb import SamDB
30 from samba.dcerpc import drsuapi, misc
32 from samba.kcc.kcc_utils import Site, Partition, Transport, SiteLink
33 from samba.kcc.kcc_utils import NCReplica, NCType, nctype_lut, GraphNode
34 from samba.kcc.kcc_utils import RepsFromTo, KCCError, KCCFailedObject
35 from samba.kcc.graph import convert_schedule_to_repltimes
37 from samba.ndr import ndr_pack
39 from samba.kcc.graph_utils import verify_and_dot
41 from samba.kcc import ldif_import_export
42 from samba.kcc.graph import setup_graph, get_spanning_tree_edges
43 from samba.kcc.graph import Vertex
45 from samba.kcc.debug import DEBUG, DEBUG_FN, logger
46 from samba.kcc import debug
49 def sort_replica_by_dsa_guid(rep1, rep2):
50 """Helper to sort NCReplicas by their DSA guids
52 The guids need to be sorted in their NDR form.
54 :param rep1: An NC replica
55 :param rep2: Another replica
56 :return: -1, 0, or 1, indicating sort order.
57 """
58 return cmp(ndr_pack(rep1.rep_dsa_guid), ndr_pack(rep2.rep_dsa_guid))
61 def sort_dsa_by_gc_and_guid(dsa1, dsa2):
62 """Helper to sort DSAs by guid global catalog status
64 GC DSAs come before non-GC DSAs, other than that, the guids are
65 sorted in NDR form.
67 :param dsa1: A DSA object
68 :param dsa2: Another DSA
69 :return: -1, 0, or 1, indicating sort order.
70 """
71 if dsa1.is_gc() and not dsa2.is_gc():
72 return -1
73 if not dsa1.is_gc() and dsa2.is_gc():
74 return +1
75 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
78 def is_smtp_replication_available():
79 """Can the KCC use SMTP replication?
81 Currently always returns false because Samba doesn't implement
82 SMTP transfer for NC changes between DCs.
84 :return: Boolean (always False)
85 """
86 return False
89 class KCC(object):
90 """The Knowledge Consistency Checker class.
92 A container for objects and methods allowing a run of the KCC. Produces a
93 set of connections in the samdb for which the Distributed Replication
94 Service can then utilize to replicate naming contexts
96 :param unix_now: The putative current time in seconds since 1970.
97 :param read_only: Don't write to the database.
98 :param verify: Check topological invariants for the generated graphs
99 :param debug: Write verbosely to stderr.
100 "param dot_file_dir: write diagnostic Graphviz files in this directory
102 def __init__(self, unix_now, readonly=False, verify=False, debug=False,
103 dot_file_dir=None):
104 """Initializes the partitions class which can hold
105 our local DCs partitions or all the partitions in
106 the forest
108 self.part_table = {} # partition objects
109 self.site_table = {}
110 self.ip_transport = None
111 self.sitelink_table = {}
112 self.dsa_by_dnstr = {}
113 self.dsa_by_guid = {}
115 self.get_dsa_by_guidstr = self.dsa_by_guid.get
116 self.get_dsa = self.dsa_by_dnstr.get
118 # TODO: These should be backed by a 'permanent' store so that when
119 # calling DRSGetReplInfo with DS_REPL_INFO_KCC_DSA_CONNECT_FAILURES,
120 # the failure information can be returned
121 self.kcc_failed_links = {}
122 self.kcc_failed_connections = set()
124 # Used in inter-site topology computation. A list
125 # of connections (by NTDSConnection object) that are
126 # to be kept when pruning un-needed NTDS Connections
127 self.kept_connections = set()
129 self.my_dsa_dnstr = None # My dsa DN
130 self.my_dsa = None # My dsa object
132 self.my_site_dnstr = None
133 self.my_site = None
135 self.samdb = None
137 self.unix_now = unix_now
138 self.nt_now = unix2nttime(unix_now)
139 self.readonly = readonly
140 self.verify = verify
141 self.debug = debug
142 self.dot_file_dir = dot_file_dir
144 def load_ip_transport(self):
145 """Loads the inter-site transport objects for Sites
147 :return: None
148 :raise KCCError: if no IP transport is found
150 try:
151 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
152 self.samdb.get_config_basedn(),
153 scope=ldb.SCOPE_SUBTREE,
154 expression="(objectClass=interSiteTransport)")
155 except ldb.LdbError, (enum, estr):
156 raise KCCError("Unable to find inter-site transports - (%s)" %
157 estr)
159 for msg in res:
160 dnstr = str(msg.dn)
162 transport = Transport(dnstr)
164 transport.load_transport(self.samdb)
165 if transport.name == 'IP':
166 self.ip_transport = transport
167 elif transport.name == 'SMTP':
168 logger.debug("Samba KCC is ignoring the obsolete "
169 "SMTP transport.")
171 else:
172 logger.warning("Samba KCC does not support the transport "
173 "called %r." % (transport.name,))
175 if self.ip_transport is None:
176 raise KCCError("there doesn't seem to be an IP transport")
178 def load_all_sitelinks(self):
179 """Loads the inter-site siteLink objects
181 :return: None
182 :raise KCCError: if site-links aren't found
184 try:
185 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
186 self.samdb.get_config_basedn(),
187 scope=ldb.SCOPE_SUBTREE,
188 expression="(objectClass=siteLink)")
189 except ldb.LdbError, (enum, estr):
190 raise KCCError("Unable to find inter-site siteLinks - (%s)" % estr)
192 for msg in res:
193 dnstr = str(msg.dn)
195 # already loaded
196 if dnstr in self.sitelink_table:
197 continue
199 sitelink = SiteLink(dnstr)
201 sitelink.load_sitelink(self.samdb)
203 # Assign this siteLink to table
204 # and index by dn
205 self.sitelink_table[dnstr] = sitelink
207 def load_site(self, dn_str):
208 """Helper for load_my_site and load_all_sites.
210 Put all the site's DSAs into the KCC indices.
212 :param dn_str: a site dn_str
213 :return: the Site object pertaining to the dn_str
215 site = Site(dn_str, self.unix_now)
216 site.load_site(self.samdb)
218 # We avoid replacing the site with an identical copy in case
219 # somewhere else has a reference to the old one, which would
220 # lead to all manner of confusion and chaos.
221 guid = str(site.site_guid)
222 if guid not in self.site_table:
223 self.site_table[guid] = site
224 self.dsa_by_dnstr.update(site.dsa_table)
225 self.dsa_by_guid.update((str(x.dsa_guid), x)
226 for x in site.dsa_table.values())
228 return self.site_table[guid]
230 def load_my_site(self):
231 """Load the Site object for the local DSA.
233 :return: None
235 self.my_site_dnstr = ("CN=%s,CN=Sites,%s" % (
236 self.samdb.server_site_name(),
237 self.samdb.get_config_basedn()))
239 self.my_site = self.load_site(self.my_site_dnstr)
241 def load_all_sites(self):
242 """Discover all sites and create Site objects.
244 :return: None
245 :raise: KCCError if sites can't be found
247 try:
248 res = self.samdb.search("CN=Sites,%s" %
249 self.samdb.get_config_basedn(),
250 scope=ldb.SCOPE_SUBTREE,
251 expression="(objectClass=site)")
252 except ldb.LdbError, (enum, estr):
253 raise KCCError("Unable to find sites - (%s)" % estr)
255 for msg in res:
256 sitestr = str(msg.dn)
257 self.load_site(sitestr)
259 def load_my_dsa(self):
260 """Discover my nTDSDSA dn thru the rootDSE entry
262 :return: None
263 :raise: KCCError if DSA can't be found
265 dn_query = "<GUID=%s>" % self.samdb.get_ntds_GUID()
266 dn = ldb.Dn(self.samdb, dn_query)
267 try:
268 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
269 attrs=["objectGUID"])
270 except ldb.LdbError, (enum, estr):
271 DEBUG_FN("Search for dn '%s' [from %s] failed: %s. "
272 "This typically happens in --importldif mode due "
273 "to lack of module support." % (dn, dn_query, estr))
274 try:
275 # We work around the failure above by looking at the
276 # dsServiceName that was put in the fake rootdse by
277 # the --exportldif, rather than the
278 # samdb.get_ntds_GUID(). The disadvantage is that this
279 # mode requires we modify the @ROOTDSE dnq to support
280 # --forced-local-dsa
281 service_name_res = self.samdb.search(base="",
282 scope=ldb.SCOPE_BASE,
283 attrs=["dsServiceName"])
284 dn = ldb.Dn(self.samdb,
285 service_name_res[0]["dsServiceName"][0])
287 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
288 attrs=["objectGUID"])
289 except ldb.LdbError, (enum, estr):
290 raise KCCError("Unable to find my nTDSDSA - (%s)" % estr)
292 if len(res) != 1:
293 raise KCCError("Unable to find my nTDSDSA at %s" %
294 dn.extended_str())
296 ntds_guid = misc.GUID(self.samdb.get_ntds_GUID())
297 if misc.GUID(res[0]["objectGUID"][0]) != ntds_guid:
298 raise KCCError("Did not find the GUID we expected,"
299 " perhaps due to --importldif")
301 self.my_dsa_dnstr = str(res[0].dn)
303 self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
305 if self.my_dsa_dnstr not in self.dsa_by_dnstr:
306 debug.DEBUG_DARK_YELLOW("my_dsa %s isn't in self.dsas_by_dnstr:"
307 " it must be RODC.\n"
308 "Let's add it, because my_dsa is special!"
309 "\n(likewise for self.dsa_by_guid)" %
310 self.my_dsa_dnstr)
312 self.dsa_by_dnstr[self.my_dsa_dnstr] = self.my_dsa
313 self.dsa_by_guid[str(self.my_dsa.dsa_guid)] = self.my_dsa
315 def load_all_partitions(self):
316 """Discover and load all partitions.
318 Each NC is inserted into the part_table by partition
319 dn string (not the nCName dn string)
321 :return: None
322 :raise: KCCError if partitions can't be found
324 try:
325 res = self.samdb.search("CN=Partitions,%s" %
326 self.samdb.get_config_basedn(),
327 scope=ldb.SCOPE_SUBTREE,
328 expression="(objectClass=crossRef)")
329 except ldb.LdbError, (enum, estr):
330 raise KCCError("Unable to find partitions - (%s)" % estr)
332 for msg in res:
333 partstr = str(msg.dn)
335 # already loaded
336 if partstr in self.part_table:
337 continue
339 part = Partition(partstr)
341 part.load_partition(self.samdb)
342 self.part_table[partstr] = part
344 def refresh_failed_links_connections(self, ping=None):
345 """Ensure the failed links list is up to date
347 Based on MS-ADTS 6.2.2.1
349 :param ping: An oracle function of remote site availability
350 :return: None
352 # LINKS: Refresh failed links
353 self.kcc_failed_links = {}
354 current, needed = self.my_dsa.get_rep_tables()
355 for replica in current.values():
356 # For every possible connection to replicate
357 for reps_from in replica.rep_repsFrom:
358 failure_count = reps_from.consecutive_sync_failures
359 if failure_count <= 0:
360 continue
362 dsa_guid = str(reps_from.source_dsa_obj_guid)
363 time_first_failure = reps_from.last_success
364 last_result = reps_from.last_attempt
365 dns_name = reps_from.dns_name1
367 f = self.kcc_failed_links.get(dsa_guid)
368 if f is None:
369 f = KCCFailedObject(dsa_guid, failure_count,
370 time_first_failure, last_result,
371 dns_name)
372 self.kcc_failed_links[dsa_guid] = f
373 else:
374 f.failure_count = max(f.failure_count, failure_count)
375 f.time_first_failure = min(f.time_first_failure,
376 time_first_failure)
377 f.last_result = last_result
379 # CONNECTIONS: Refresh failed connections
380 restore_connections = set()
381 if ping is not None:
382 DEBUG("refresh_failed_links: checking if links are still down")
383 for connection in self.kcc_failed_connections:
384 if ping(connection.dns_name):
385 # Failed connection is no longer failing
386 restore_connections.add(connection)
387 else:
388 connection.failure_count += 1
389 else:
390 DEBUG("refresh_failed_links: not checking live links because we\n"
391 "weren't asked to --attempt-live-connections")
393 # Remove the restored connections from the failed connections
394 self.kcc_failed_connections.difference_update(restore_connections)
396 def is_stale_link_connection(self, target_dsa):
397 """Check whether a link to a remote DSA is stale
399 Used in MS-ADTS 6.2.2.2 Intrasite Connection Creation
401 Returns True if the remote seems to have been down for at
402 least two hours, otherwise False.
404 :param target_dsa: the remote DSA object
405 :return: True if link is stale, otherwise False
407 failed_link = self.kcc_failed_links.get(str(target_dsa.dsa_guid))
408 if failed_link:
409 # failure_count should be > 0, but check anyways
410 if failed_link.failure_count > 0:
411 unix_first_failure = \
412 nttime2unix(failed_link.time_first_failure)
413 # TODO guard against future
414 if unix_first_failure > self.unix_now:
415 logger.error("The last success time attribute for \
416 repsFrom is in the future!")
418 # Perform calculation in seconds
419 if (self.unix_now - unix_first_failure) > 60 * 60 * 2:
420 return True
422 # TODO connections.
423 # We have checked failed *links*, but we also need to check
424 # *connections*
426 return False
428 # TODO: This should be backed by some form of local database
429 def remove_unneeded_failed_links_connections(self):
430 # Remove all tuples in kcc_failed_links where failure count = 0
431 # In this implementation, this should never happen.
433 # Remove all connections which were not used this run or connections
434 # that became active during this run.
435 pass
437 def _ensure_connections_are_loaded(self, connections):
438 """Load or fake-load NTDSConnections lacking GUIDs
440 New connections don't have GUIDs and created times which are
441 needed for sorting. If we're in read-only mode, we make fake
442 GUIDs, otherwise we ask SamDB to do it for us.
444 :param connections: an iterable of NTDSConnection objects.
445 :return: None
447 for cn_conn in connections:
448 if cn_conn.guid is None:
449 if self.readonly:
450 cn_conn.guid = misc.GUID(str(uuid.uuid4()))
451 cn_conn.whenCreated = self.nt_now
452 else:
453 cn_conn.load_connection(self.samdb)
455 def _mark_broken_ntdsconn(self):
456 """Find NTDS Connections that lack a remote
458 I'm not sure how they appear. Let's be rid of them by marking
459 them with the to_be_deleted attribute.
461 :return: None
463 for cn_conn in self.my_dsa.connect_table.values():
464 s_dnstr = cn_conn.get_from_dnstr()
465 if s_dnstr is None:
466 DEBUG_FN("%s has phantom connection %s" % (self.my_dsa,
467 cn_conn))
468 cn_conn.to_be_deleted = True
470 def _mark_unneeded_local_ntdsconn(self):
471 """Find unneeded intrasite NTDS Connections for removal
473 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections.
474 Every DC removes its own unnecessary intrasite connections.
475 This function tags them with the to_be_deleted attribute.
477 :return: None
479 # XXX should an RODC be regarded as same site? It isn't part
480 # of the intrasite ring.
482 if self.my_site.is_cleanup_ntdsconn_disabled():
483 DEBUG_FN("not doing ntdsconn cleanup for site %s, "
484 "because it is disabled" % self.my_site)
485 return
487 mydsa = self.my_dsa
489 try:
490 self._ensure_connections_are_loaded(mydsa.connect_table.values())
491 except KCCError:
492 # RODC never actually added any connections to begin with
493 if mydsa.is_ro():
494 return
496 local_connections = []
498 for cn_conn in mydsa.connect_table.values():
499 s_dnstr = cn_conn.get_from_dnstr()
500 if s_dnstr in self.my_site.dsa_table:
501 removable = not (cn_conn.is_generated() or
502 cn_conn.is_rodc_topology())
503 packed_guid = ndr_pack(cn_conn.guid)
504 local_connections.append((cn_conn, s_dnstr,
505 packed_guid, removable))
507 for a, b in itertools.permutations(local_connections, 2):
508 cn_conn, s_dnstr, packed_guid, removable = a
509 cn_conn2, s_dnstr2, packed_guid2, removable2 = b
510 if (removable and
511 s_dnstr == s_dnstr2 and
512 cn_conn.whenCreated < cn_conn2.whenCreated or
513 (cn_conn.whenCreated == cn_conn2.whenCreated and
514 packed_guid < packed_guid2)):
515 cn_conn.to_be_deleted = True
517 def _mark_unneeded_intersite_ntdsconn(self):
518 """find unneeded intersite NTDS Connections for removal
520 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections. The
521 intersite topology generator removes links for all DCs in its
522 site. Here we just tag them with the to_be_deleted attribute.
524 :return: None
526 # TODO Figure out how best to handle the RODC case
527 # The RODC is ITSG, but shouldn't act on anyone's behalf.
528 if self.my_dsa.is_ro():
529 return
531 # Find the intersite connections
532 local_dsas = self.my_site.dsa_table
533 connections_and_dsas = []
534 for dsa in local_dsas.values():
535 for cn in dsa.connect_table.values():
536 if cn.to_be_deleted:
537 continue
538 s_dnstr = cn.get_from_dnstr()
539 if s_dnstr is None:
540 continue
541 if s_dnstr not in local_dsas:
542 from_dsa = self.get_dsa(s_dnstr)
543 # Samba ONLY: ISTG removes connections to dead DCs
544 if from_dsa is None and '\\0ADEL' in s_dnstr:
545 logger.info("DSA appears deleted, removing connection %s" % s_dnstr)
546 cn.to_be_deleted = True
547 continue
548 connections_and_dsas.append((cn, dsa, from_dsa))
550 self._ensure_connections_are_loaded(x[0] for x in connections_and_dsas)
551 for cn, to_dsa, from_dsa in connections_and_dsas:
552 if not cn.is_generated() or cn.is_rodc_topology():
553 continue
555 # If the connection is in the kept_connections list, we
556 # only remove it if an endpoint seems down.
557 if (cn in self.kept_connections and
558 not (self.is_bridgehead_failed(to_dsa, True) or
559 self.is_bridgehead_failed(from_dsa, True))):
560 continue
562 # this one is broken and might be superseded by another.
563 # But which other? Let's just say another link to the same
564 # site can supersede.
565 from_dnstr = from_dsa.dsa_dnstr
566 for site in self.site_table.values():
567 if from_dnstr in site.rw_dsa_table:
568 for cn2, to_dsa2, from_dsa2 in connections_and_dsas:
569 if (cn is not cn2 and
570 from_dsa2 in site.rw_dsa_table):
571 cn.to_be_deleted = True
573 def _commit_changes(self, dsa):
574 if dsa.is_ro() or self.readonly:
575 for connect in dsa.connect_table.values():
576 if connect.to_be_deleted:
577 logger.info("TO BE DELETED:\n%s" % connect)
578 if connect.to_be_added:
579 logger.info("TO BE ADDED:\n%s" % connect)
580 if connect.to_be_modified:
581 logger.info("TO BE MODIFIED:\n%s" % connect)
583 # Peform deletion from our tables but perform
584 # no database modification
585 dsa.commit_connections(self.samdb, ro=True)
586 else:
587 # Commit any modified connections
588 dsa.commit_connections(self.samdb)
590 def remove_unneeded_ntdsconn(self, all_connected):
591 """Remove unneeded NTDS Connections once topology is calculated
593 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections
595 :param all_connected: indicates whether all sites are connected
596 :return: None
598 self._mark_broken_ntdsconn()
599 self._mark_unneeded_local_ntdsconn()
600 # if we are not the istg, we're done!
601 # if we are the istg, but all_connected is False, we also do nothing.
602 if self.my_dsa.is_istg() and all_connected:
603 self._mark_unneeded_intersite_ntdsconn()
605 for dsa in self.my_site.dsa_table.values():
606 self._commit_changes(dsa)
608 def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
609 """Update an repsFrom object if required.
611 Part of MS-ADTS 6.2.2.5.
613 Update t_repsFrom if necessary to satisfy requirements. Such
614 updates are typically required when the IDL_DRSGetNCChanges
615 server has moved from one site to another--for example, to
616 enable compression when the server is moved from the
617 client's site to another site.
619 The repsFrom.update_flags bit field may be modified
620 auto-magically if any changes are made here. See
621 kcc_utils.RepsFromTo for gory details.
624 :param n_rep: NC replica we need
625 :param t_repsFrom: repsFrom tuple to modify
626 :param s_rep: NC replica at source DSA
627 :param s_dsa: source DSA
628 :param cn_conn: Local DSA NTDSConnection child
630 :return: None
632 s_dnstr = s_dsa.dsa_dnstr
633 same_site = s_dnstr in self.my_site.dsa_table
635 # if schedule doesn't match then update and modify
636 times = convert_schedule_to_repltimes(cn_conn.schedule)
637 if times != t_repsFrom.schedule:
638 t_repsFrom.schedule = times
640 # Bit DRS_ADD_REF is set in replicaFlags unconditionally
641 # Samba ONLY:
642 if ((t_repsFrom.replica_flags &
643 drsuapi.DRSUAPI_DRS_ADD_REF) == 0x0):
644 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_ADD_REF
646 # Bit DRS_PER_SYNC is set in replicaFlags if and only
647 # if nTDSConnection schedule has a value v that specifies
648 # scheduled replication is to be performed at least once
649 # per week.
650 if cn_conn.is_schedule_minimum_once_per_week():
652 if ((t_repsFrom.replica_flags &
653 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0):
654 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
656 # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
657 # if the source DSA and the local DC's nTDSDSA object are
658 # in the same site or source dsa is the FSMO role owner
659 # of one or more FSMO roles in the NC replica.
660 if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
662 if ((t_repsFrom.replica_flags &
663 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0):
664 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
666 # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
667 # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
668 # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
669 # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
670 # t.replicaFlags if and only if s and the local DC's
671 # nTDSDSA object are in different sites.
672 if ((cn_conn.options &
673 dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0):
675 if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
676 # WARNING
678 # it LOOKS as if this next test is a bit silly: it
679 # checks the flag then sets it if it not set; the same
680 # effect could be achieved by unconditionally setting
681 # it. But in fact the repsFrom object has special
682 # magic attached to it, and altering replica_flags has
683 # side-effects. That is bad in my opinion, but there
684 # you go.
685 if ((t_repsFrom.replica_flags &
686 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
687 t_repsFrom.replica_flags |= \
688 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
690 elif not same_site:
692 if ((t_repsFrom.replica_flags &
693 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
694 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
696 # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
697 # and only if s and the local DC's nTDSDSA object are
698 # not in the same site and the
699 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
700 # clear in cn!options
701 if (not same_site and
702 (cn_conn.options &
703 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
705 if ((t_repsFrom.replica_flags &
706 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0):
707 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
709 # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
710 # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
711 if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
713 if ((t_repsFrom.replica_flags &
714 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0):
715 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
717 # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
718 # set in t.replicaFlags if and only if cn!enabledConnection = false.
719 if not cn_conn.is_enabled():
721 if ((t_repsFrom.replica_flags &
722 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0):
723 t_repsFrom.replica_flags |= \
724 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
726 if ((t_repsFrom.replica_flags &
727 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0):
728 t_repsFrom.replica_flags |= \
729 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
731 # If s and the local DC's nTDSDSA object are in the same site,
732 # cn!transportType has no value, or the RDN of cn!transportType
733 # is CN=IP:
735 # Bit DRS_MAIL_REP in t.replicaFlags is clear.
737 # t.uuidTransport = NULL GUID.
739 # t.uuidDsa = The GUID-based DNS name of s.
741 # Otherwise:
743 # Bit DRS_MAIL_REP in t.replicaFlags is set.
745 # If x is the object with dsname cn!transportType,
746 # t.uuidTransport = x!objectGUID.
748 # Let a be the attribute identified by
749 # x!transportAddressAttribute. If a is
750 # the dNSHostName attribute, t.uuidDsa = the GUID-based
751 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
753 # It appears that the first statement i.e.
755 # "If s and the local DC's nTDSDSA object are in the same
756 # site, cn!transportType has no value, or the RDN of
757 # cn!transportType is CN=IP:"
759 # could be a slightly tighter statement if it had an "or"
760 # between each condition. I believe this should
761 # be interpreted as:
763 # IF (same-site) OR (no-value) OR (type-ip)
765 # because IP should be the primary transport mechanism
766 # (even in inter-site) and the absense of the transportType
767 # attribute should always imply IP no matter if its multi-site
769 # NOTE MS-TECH INCORRECT:
771 # All indications point to these statements above being
772 # incorrectly stated:
774 # t.uuidDsa = The GUID-based DNS name of s.
776 # Let a be the attribute identified by
777 # x!transportAddressAttribute. If a is
778 # the dNSHostName attribute, t.uuidDsa = the GUID-based
779 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
781 # because the uuidDSA is a GUID and not a GUID-base DNS
782 # name. Nor can uuidDsa hold (s!parent)!a if not
783 # dNSHostName. What should have been said is:
785 # t.naDsa = The GUID-based DNS name of s
787 # That would also be correct if transportAddressAttribute
788 # were "mailAddress" because (naDsa) can also correctly
789 # hold the SMTP ISM service address.
791 nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
793 if ((t_repsFrom.replica_flags &
794 drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0):
795 t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
797 t_repsFrom.transport_guid = misc.GUID()
799 # See (NOTE MS-TECH INCORRECT) above
801 # NOTE: it looks like these conditionals are pointless,
802 # because the state will end up as `t_repsFrom.dns_name1 ==
803 # nastr` in either case, BUT the repsFrom thing is magic and
804 # assigning to it alters some flags. So we try not to update
805 # it unless necessary.
806 if t_repsFrom.dns_name1 != nastr:
807 t_repsFrom.dns_name1 = nastr
809 if t_repsFrom.version > 0x1 and t_repsFrom.dns_name2 != nastr:
810 t_repsFrom.dns_name2 = nastr
812 if t_repsFrom.is_modified():
813 DEBUG_FN("modify_repsFrom(): %s" % t_repsFrom)
815 def get_dsa_for_implied_replica(self, n_rep, cn_conn):
816 """If a connection imply a replica, find the relevant DSA
818 Given a NC replica and NTDS Connection, determine if the
819 connection implies a repsFrom tuple should be present from the
820 source DSA listed in the connection to the naming context. If
821 it should be, return the DSA; otherwise return None.
823 Based on part of MS-ADTS 6.2.2.5
825 :param n_rep: NC replica
826 :param cn_conn: NTDS Connection
827 :return: source DSA or None
829 # XXX different conditions for "implies" than MS-ADTS 6.2.2
830 # preamble.
832 # It boils down to: we want an enabled, non-FRS connections to
833 # a valid remote DSA with a non-RO replica corresponding to
834 # n_rep.
836 if not cn_conn.is_enabled() or cn_conn.is_rodc_topology():
837 return None
839 s_dnstr = cn_conn.get_from_dnstr()
840 s_dsa = self.get_dsa(s_dnstr)
842 # No DSA matching this source DN string?
843 if s_dsa is None:
844 return None
846 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
848 if (s_rep is not None and
849 s_rep.is_present() and
850 (not s_rep.is_ro() or n_rep.is_partial())):
851 return s_dsa
852 return None
854 def translate_ntdsconn(self, current_dsa=None):
855 """Adjust repsFrom to match NTDSConnections
857 This function adjusts values of repsFrom abstract attributes of NC
858 replicas on the local DC to match those implied by
859 nTDSConnection objects.
861 Based on [MS-ADTS] 6.2.2.5
863 :param current_dsa: optional DSA on whose behalf we are acting.
864 :return: None
866 count = 0
868 ro = False
869 if current_dsa is None:
870 current_dsa = self.my_dsa
872 if current_dsa.is_ro():
873 ro = True
875 if current_dsa.is_translate_ntdsconn_disabled():
876 DEBUG_FN("skipping translate_ntdsconn() "
877 "because disabling flag is set")
878 return
880 DEBUG_FN("translate_ntdsconn(): enter")
882 current_rep_table, needed_rep_table = current_dsa.get_rep_tables()
884 # Filled in with replicas we currently have that need deleting
885 delete_reps = set()
887 # We're using the MS notation names here to allow
888 # correlation back to the published algorithm.
890 # n_rep - NC replica (n)
891 # t_repsFrom - tuple (t) in n!repsFrom
892 # s_dsa - Source DSA of the replica. Defined as nTDSDSA
893 # object (s) such that (s!objectGUID = t.uuidDsa)
894 # In our IDL representation of repsFrom the (uuidDsa)
895 # attribute is called (source_dsa_obj_guid)
896 # cn_conn - (cn) is nTDSConnection object and child of the local
897 # DC's nTDSDSA object and (cn!fromServer = s)
898 # s_rep - source DSA replica of n
900 # If we have the replica and its not needed
901 # then we add it to the "to be deleted" list.
902 for dnstr in current_rep_table:
903 # If we're on the RODC, hardcode the update flags
904 if ro:
905 c_rep = current_rep_table[dnstr]
906 c_rep.load_repsFrom(self.samdb)
907 for t_repsFrom in c_rep.rep_repsFrom:
908 replica_flags = (drsuapi.DRSUAPI_DRS_INIT_SYNC |
909 drsuapi.DRSUAPI_DRS_PER_SYNC |
910 drsuapi.DRSUAPI_DRS_ADD_REF |
911 drsuapi.DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING |
912 drsuapi.DRSUAPI_DRS_GET_ALL_GROUP_MEMBERSHIP |
913 drsuapi.DRSUAPI_DRS_NONGC_RO_REP)
914 if t_repsFrom.replica_flags != replica_flags:
915 t_repsFrom.replica_flags = replica_flags
916 c_rep.commit_repsFrom(self.samdb)
917 else:
918 if dnstr not in needed_rep_table:
919 delete_reps.add(dnstr)
921 DEBUG_FN('current %d needed %d delete %d' % (len(current_rep_table),
922 len(needed_rep_table), len(delete_reps)))
924 if delete_reps:
925 DEBUG('deleting these reps: %s' % delete_reps)
926 for dnstr in delete_reps:
927 del current_rep_table[dnstr]
929 # Now perform the scan of replicas we'll need
930 # and compare any current repsFrom against the
931 # connections
932 for n_rep in needed_rep_table.values():
934 # load any repsFrom and fsmo roles as we'll
935 # need them during connection translation
936 n_rep.load_repsFrom(self.samdb)
937 n_rep.load_fsmo_roles(self.samdb)
939 # Loop thru the existing repsFrom tupples (if any)
940 # XXX This is a list and could contain duplicates
941 # (multiple load_repsFrom calls)
942 for t_repsFrom in n_rep.rep_repsFrom:
944 # for each tuple t in n!repsFrom, let s be the nTDSDSA
945 # object such that s!objectGUID = t.uuidDsa
946 guidstr = str(t_repsFrom.source_dsa_obj_guid)
947 s_dsa = self.get_dsa_by_guidstr(guidstr)
949 # Source dsa is gone from config (strange)
950 # so cleanup stale repsFrom for unlisted DSA
951 if s_dsa is None:
952 logger.warning("repsFrom source DSA guid (%s) not found" %
953 guidstr)
954 t_repsFrom.to_be_deleted = True
955 continue
957 # Find the connection that this repsFrom would use. If
958 # there isn't a good one (i.e. non-RODC_TOPOLOGY,
959 # meaning non-FRS), we delete the repsFrom.
960 s_dnstr = s_dsa.dsa_dnstr
961 connections = current_dsa.get_connection_by_from_dnstr(s_dnstr)
962 for cn_conn in connections:
963 if not cn_conn.is_rodc_topology():
964 break
965 else:
966 # no break means no non-rodc_topology connection exists
967 t_repsFrom.to_be_deleted = True
968 continue
970 # KCC removes this repsFrom tuple if any of the following
971 # is true:
972 # No NC replica of the NC "is present" on DSA that
973 # would be source of replica
975 # A writable replica of the NC "should be present" on
976 # the local DC, but a partial replica "is present" on
977 # the source DSA
978 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
980 if s_rep is None or not s_rep.is_present() or \
981 (not n_rep.is_ro() and s_rep.is_partial()):
983 t_repsFrom.to_be_deleted = True
984 continue
986 # If the KCC did not remove t from n!repsFrom, it updates t
987 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
989 # Loop thru connections and add implied repsFrom tuples
990 # for each NTDSConnection under our local DSA if the
991 # repsFrom is not already present
992 for cn_conn in current_dsa.connect_table.values():
994 s_dsa = self.get_dsa_for_implied_replica(n_rep, cn_conn)
995 if s_dsa is None:
996 continue
998 # Loop thru the existing repsFrom tupples (if any) and
999 # if we already have a tuple for this connection then
1000 # no need to proceed to add. It will have been changed
1001 # to have the correct attributes above
1002 for t_repsFrom in n_rep.rep_repsFrom:
1003 guidstr = str(t_repsFrom.source_dsa_obj_guid)
1004 if s_dsa is self.get_dsa_by_guidstr(guidstr):
1005 s_dsa = None
1006 break
1008 if s_dsa is None:
1009 continue
1011 # Create a new RepsFromTo and proceed to modify
1012 # it according to specification
1013 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
1015 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
1017 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
1019 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
1021 # Add to our NC repsFrom as this is newly computed
1022 if t_repsFrom.is_modified():
1023 n_rep.rep_repsFrom.append(t_repsFrom)
1025 if self.readonly or ro:
1026 # Display any to be deleted or modified repsFrom
1027 text = n_rep.dumpstr_to_be_deleted()
1028 if text:
1029 logger.info("TO BE DELETED:\n%s" % text)
1030 text = n_rep.dumpstr_to_be_modified()
1031 if text:
1032 logger.info("TO BE MODIFIED:\n%s" % text)
1034 # Peform deletion from our tables but perform
1035 # no database modification
1036 n_rep.commit_repsFrom(self.samdb, ro=True)
1037 else:
1038 # Commit any modified repsFrom to the NC replica
1039 n_rep.commit_repsFrom(self.samdb)
1041 def merge_failed_links(self, ping=None):
1042 """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
1044 The KCC on a writable DC attempts to merge the link and connection
1045 failure information from bridgehead DCs in its own site to help it
1046 identify failed bridgehead DCs.
1048 Based on MS-ADTS 6.2.2.3.2 "Merge of kCCFailedLinks and kCCFailedLinks
1049 from Bridgeheads"
1051 :param ping: An oracle of current bridgehead availability
1052 :return: None
1054 # 1. Queries every bridgehead server in your site (other than yourself)
1055 # 2. For every ntDSConnection that references a server in a different
1056 # site merge all the failure info
1058 # XXX - not implemented yet
1059 if ping is not None:
1060 debug.DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1061 else:
1062 DEBUG_FN("skipping merge_failed_links() because it requires "
1063 "real network connections\n"
1064 "and we weren't asked to --attempt-live-connections")
1066 def setup_graph(self, part):
1067 """Set up an intersite graph
1069 An intersite graph has a Vertex for each site object, a
1070 MultiEdge for each SiteLink object, and a MutliEdgeSet for
1071 each siteLinkBridge object (or implied siteLinkBridge). It
1072 reflects the intersite topology in a slightly more abstract
1073 graph form.
1075 Roughly corresponds to MS-ADTS 6.2.2.3.4.3
1077 :param part: a Partition object
1078 :returns: an InterSiteGraph object
1080 # If 'Bridge all site links' is enabled and Win2k3 bridges required
1081 # is not set
1082 # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1083 # No documentation for this however, ntdsapi.h appears to have:
1084 # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1085 bridges_required = self.my_site.site_options & 0x00001002 != 0
1086 transport_guid = str(self.ip_transport.guid)
1088 g = setup_graph(part, self.site_table, transport_guid,
1089 self.sitelink_table, bridges_required)
1091 if self.verify or self.dot_file_dir is not None:
1092 dot_edges = []
1093 for edge in g.edges:
1094 for a, b in itertools.combinations(edge.vertices, 2):
1095 dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1096 verify_properties = ()
1097 name = 'site_edges_%s' % part.partstr
1098 verify_and_dot(name, dot_edges, directed=False,
1099 label=self.my_dsa_dnstr,
1100 properties=verify_properties, debug=DEBUG,
1101 verify=self.verify,
1102 dot_file_dir=self.dot_file_dir)
1104 return g
1106 def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1107 """Get a bridghead DC for a site.
1109 Part of MS-ADTS 6.2.2.3.4.4
1111 :param site: site object representing for which a bridgehead
1112 DC is desired.
1113 :param part: crossRef for NC to replicate.
1114 :param transport: interSiteTransport object for replication
1115 traffic.
1116 :param partial_ok: True if a DC containing a partial
1117 replica or a full replica will suffice, False if only
1118 a full replica will suffice.
1119 :param detect_failed: True to detect failed DCs and route
1120 replication traffic around them, False to assume no DC
1121 has failed.
1122 :return: dsa object for the bridgehead DC or None
1125 bhs = self.get_all_bridgeheads(site, part, transport,
1126 partial_ok, detect_failed)
1127 if not bhs:
1128 debug.DEBUG_MAGENTA("get_bridgehead FAILED:\nsitedn = %s" %
1129 site.site_dnstr)
1130 return None
1132 debug.DEBUG_GREEN("get_bridgehead:\n\tsitedn = %s\n\tbhdn = %s" %
1133 (site.site_dnstr, bhs[0].dsa_dnstr))
1134 return bhs[0]
1136 def get_all_bridgeheads(self, site, part, transport,
1137 partial_ok, detect_failed):
1138 """Get all bridghead DCs on a site satisfying the given criteria
1140 Part of MS-ADTS 6.2.2.3.4.4
1142 :param site: site object representing the site for which
1143 bridgehead DCs are desired.
1144 :param part: partition for NC to replicate.
1145 :param transport: interSiteTransport object for
1146 replication traffic.
1147 :param partial_ok: True if a DC containing a partial
1148 replica or a full replica will suffice, False if
1149 only a full replica will suffice.
1150 :param detect_failed: True to detect failed DCs and route
1151 replication traffic around them, FALSE to assume
1152 no DC has failed.
1153 :return: list of dsa object for available bridgehead DCs
1155 bhs = []
1157 if transport.name != "IP":
1158 raise KCCError("get_all_bridgeheads has run into a "
1159 "non-IP transport! %r"
1160 % (transport.name,))
1162 DEBUG_FN(site.rw_dsa_table)
1163 for dsa in site.rw_dsa_table.values():
1165 pdnstr = dsa.get_parent_dnstr()
1167 # IF t!bridgeheadServerListBL has one or more values and
1168 # t!bridgeheadServerListBL does not contain a reference
1169 # to the parent object of dc then skip dc
1170 if ((len(transport.bridgehead_list) != 0 and
1171 pdnstr not in transport.bridgehead_list)):
1172 continue
1174 # IF dc is in the same site as the local DC
1175 # IF a replica of cr!nCName is not in the set of NC replicas
1176 # that "should be present" on dc or a partial replica of the
1177 # NC "should be present" but partialReplicasOkay = FALSE
1178 # Skip dc
1179 if self.my_site.same_site(dsa):
1180 needed, ro, partial = part.should_be_present(dsa)
1181 if not needed or (partial and not partial_ok):
1182 continue
1183 rep = dsa.get_current_replica(part.nc_dnstr)
1185 # ELSE
1186 # IF an NC replica of cr!nCName is not in the set of NC
1187 # replicas that "are present" on dc or a partial replica of
1188 # the NC "is present" but partialReplicasOkay = FALSE
1189 # Skip dc
1190 else:
1191 rep = dsa.get_current_replica(part.nc_dnstr)
1192 if rep is None or (rep.is_partial() and not partial_ok):
1193 continue
1195 # IF AmIRODC() and cr!nCName corresponds to default NC then
1196 # Let dsaobj be the nTDSDSA object of the dc
1197 # IF dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1198 # Skip dc
1199 if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1200 if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1201 continue
1203 # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1204 # Skip dc
1205 if self.is_bridgehead_failed(dsa, detect_failed):
1206 DEBUG("bridgehead is failed")
1207 continue
1209 DEBUG_FN("found a bridgehead: %s" % dsa.dsa_dnstr)
1210 bhs.append(dsa)
1212 # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1213 # s!options
1214 # SORT bhs such that all GC servers precede DCs that are not GC
1215 # servers, and otherwise by ascending objectGUID
1216 # ELSE
1217 # SORT bhs in a random order
1218 if site.is_random_bridgehead_disabled():
1219 bhs.sort(sort_dsa_by_gc_and_guid)
1220 else:
1221 random.shuffle(bhs)
1222 debug.DEBUG_YELLOW(bhs)
1223 return bhs
1225 def is_bridgehead_failed(self, dsa, detect_failed):
1226 """Determine whether a given DC is known to be in a failed state
1228 :param dsa: the bridgehead to test
1229 :param detect_failed: True to really check, False to assume no failure
1230 :return: True if and only if the DC should be considered failed
1232 Here we DEPART from the pseudo code spec which appears to be
1233 wrong. It says, in full:
1235 /***** BridgeheadDCFailed *****/
1236 /* Determine whether a given DC is known to be in a failed state.
1237 * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1238 * IN: detectFailedDCs - TRUE if and only failed DC detection is
1239 * enabled.
1240 * RETURNS: TRUE if and only if the DC should be considered to be in a
1241 * failed state.
1243 BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1245 IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1246 the options attribute of the site settings object for the local
1247 DC's site
1248 RETURN FALSE
1249 ELSEIF a tuple z exists in the kCCFailedLinks or
1250 kCCFailedConnections variables such that z.UUIDDsa =
1251 objectGUID, z.FailureCount > 1, and the current time -
1252 z.TimeFirstFailure > 2 hours
1253 RETURN TRUE
1254 ELSE
1255 RETURN detectFailedDCs
1256 ENDIF
1259 where you will see detectFailedDCs is not behaving as
1260 advertised -- it is acting as a default return code in the
1261 event that a failure is not detected, not a switch turning
1262 detection on or off. Elsewhere the documentation seems to
1263 concur with the comment rather than the code.
1265 if not detect_failed:
1266 return False
1268 # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1269 # When DETECT_STALE_DISABLED, we can never know of if
1270 # it's in a failed state
1271 if self.my_site.site_options & 0x00000008:
1272 return False
1274 return self.is_stale_link_connection(dsa)
1276 def create_connection(self, part, rbh, rsite, transport,
1277 lbh, lsite, link_opt, link_sched,
1278 partial_ok, detect_failed):
1279 """Create an nTDSConnection object as specified if it doesn't exist.
1281 Part of MS-ADTS 6.2.2.3.4.5
1283 :param part: crossRef object for the NC to replicate.
1284 :param rbh: nTDSDSA object for DC to act as the
1285 IDL_DRSGetNCChanges server (which is in a site other
1286 than the local DC's site).
1287 :param rsite: site of the rbh
1288 :param transport: interSiteTransport object for the transport
1289 to use for replication traffic.
1290 :param lbh: nTDSDSA object for DC to act as the
1291 IDL_DRSGetNCChanges client (which is in the local DC's site).
1292 :param lsite: site of the lbh
1293 :param link_opt: Replication parameters (aggregated siteLink options,
1294 etc.)
1295 :param link_sched: Schedule specifying the times at which
1296 to begin replicating.
1297 :partial_ok: True if bridgehead DCs containing partial
1298 replicas of the NC are acceptable.
1299 :param detect_failed: True to detect failed DCs and route
1300 replication traffic around them, FALSE to assume no DC
1301 has failed.
1303 rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1304 partial_ok, False)
1305 rbh_table = dict((x.dsa_dnstr, x) for x in rbhs_all)
1307 debug.DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all),
1308 [x.dsa_dnstr for x in rbhs_all]))
1310 # MS-TECH says to compute rbhs_avail but then doesn't use it
1311 # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1312 # partial_ok, detect_failed)
1314 lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1315 partial_ok, False)
1316 if lbh.is_ro():
1317 lbhs_all.append(lbh)
1319 debug.DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all),
1320 [x.dsa_dnstr for x in lbhs_all]))
1322 # MS-TECH says to compute lbhs_avail but then doesn't use it
1323 # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1324 # partial_ok, detect_failed)
1326 # FOR each nTDSConnection object cn such that the parent of cn is
1327 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1328 for ldsa in lbhs_all:
1329 for cn in ldsa.connect_table.values():
1331 rdsa = rbh_table.get(cn.from_dnstr)
1332 if rdsa is None:
1333 continue
1335 debug.DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1336 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1337 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1338 # cn!transportType references t
1339 if ((cn.is_generated() and
1340 not cn.is_rodc_topology() and
1341 cn.transport_guid == transport.guid)):
1343 # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1344 # cn!options and cn!schedule != sch
1345 # Perform an originating update to set cn!schedule to
1346 # sched
1347 if ((not cn.is_user_owned_schedule() and
1348 not cn.is_equivalent_schedule(link_sched))):
1349 cn.schedule = link_sched
1350 cn.set_modified(True)
1352 # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1353 # NTDSCONN_OPT_USE_NOTIFY are set in cn
1354 if cn.is_override_notify_default() and \
1355 cn.is_use_notify():
1357 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1358 # ri.Options
1359 # Perform an originating update to clear bits
1360 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1361 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1362 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1363 cn.options &= \
1364 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1365 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1366 cn.set_modified(True)
1368 # ELSE
1369 else:
1371 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1372 # ri.Options
1373 # Perform an originating update to set bits
1374 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1375 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1376 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1377 cn.options |= \
1378 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1379 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1380 cn.set_modified(True)
1382 # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1383 if cn.is_twoway_sync():
1385 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1386 # ri.Options
1387 # Perform an originating update to clear bit
1388 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1389 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1390 cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1391 cn.set_modified(True)
1393 # ELSE
1394 else:
1396 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1397 # ri.Options
1398 # Perform an originating update to set bit
1399 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1400 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1401 cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1402 cn.set_modified(True)
1404 # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1405 # in cn!options
1406 if cn.is_intersite_compression_disabled():
1408 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1409 # in ri.Options
1410 # Perform an originating update to clear bit
1411 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1412 # cn!options
1413 if ((link_opt &
1414 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0):
1415 cn.options &= \
1416 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1417 cn.set_modified(True)
1419 # ELSE
1420 else:
1421 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1422 # ri.Options
1423 # Perform an originating update to set bit
1424 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1425 # cn!options
1426 if ((link_opt &
1427 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1428 cn.options |= \
1429 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1430 cn.set_modified(True)
1432 # Display any modified connection
1433 if self.readonly:
1434 if cn.to_be_modified:
1435 logger.info("TO BE MODIFIED:\n%s" % cn)
1437 ldsa.commit_connections(self.samdb, ro=True)
1438 else:
1439 ldsa.commit_connections(self.samdb)
1440 # ENDFOR
1442 valid_connections = 0
1444 # FOR each nTDSConnection object cn such that cn!parent is
1445 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1446 for ldsa in lbhs_all:
1447 for cn in ldsa.connect_table.values():
1449 rdsa = rbh_table.get(cn.from_dnstr)
1450 if rdsa is None:
1451 continue
1453 debug.DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1455 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1456 # cn!transportType references t) and
1457 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1458 if (((not cn.is_generated() or
1459 cn.transport_guid == transport.guid) and
1460 not cn.is_rodc_topology())):
1462 # LET rguid be the objectGUID of the nTDSDSA object
1463 # referenced by cn!fromServer
1464 # LET lguid be (cn!parent)!objectGUID
1466 # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1467 # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1468 # Increment cValidConnections by 1
1469 if ((not self.is_bridgehead_failed(rdsa, detect_failed) and
1470 not self.is_bridgehead_failed(ldsa, detect_failed))):
1471 valid_connections += 1
1473 # IF keepConnections does not contain cn!objectGUID
1474 # APPEND cn!objectGUID to keepConnections
1475 self.kept_connections.add(cn)
1477 # ENDFOR
1478 debug.DEBUG_RED("valid connections %d" % valid_connections)
1479 DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1480 # IF cValidConnections = 0
1481 if valid_connections == 0:
1483 # LET opt be NTDSCONN_OPT_IS_GENERATED
1484 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1486 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1487 # SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1488 # NTDSCONN_OPT_USE_NOTIFY in opt
1489 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1490 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1491 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1493 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1494 # SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1495 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1496 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1498 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1499 # ri.Options
1500 # SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1501 if ((link_opt &
1502 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1503 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1505 # Perform an originating update to create a new nTDSConnection
1506 # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1507 # cn!options = opt, cn!transportType is a reference to t,
1508 # cn!fromServer is a reference to rbh, and cn!schedule = sch
1509 DEBUG_FN("new connection, KCC dsa: %s" % self.my_dsa.dsa_dnstr)
1510 system_flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1511 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1513 cn = lbh.new_connection(opt, system_flags, transport,
1514 rbh.dsa_dnstr, link_sched)
1516 # Display any added connection
1517 if self.readonly:
1518 if cn.to_be_added:
1519 logger.info("TO BE ADDED:\n%s" % cn)
1521 lbh.commit_connections(self.samdb, ro=True)
1522 else:
1523 lbh.commit_connections(self.samdb)
1525 # APPEND cn!objectGUID to keepConnections
1526 self.kept_connections.add(cn)
1528 def add_transports(self, vertex, local_vertex, graph, detect_failed):
1529 """Build a Vertex's transport lists
1531 Each vertex has accept_red_red and accept_black lists that
1532 list what transports they accept under various conditions. The
1533 only transport that is ever accepted is IP, and a dummy extra
1534 transport called "EDGE_TYPE_ALL".
1536 Part of MS-ADTS 6.2.2.3.4.3 -- ColorVertices
1538 :param vertex: the remote vertex we are thinking about
1539 :param local_vertex: the vertex relating to the local site.
1540 :param graph: the intersite graph
1541 :param detect_failed: whether to detect failed links
1542 :return: True if some bridgeheads were not found
1544 # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1545 # here, but using vertex seems to make more sense. That is,
1546 # the docs want this:
1548 #bh = self.get_bridgehead(local_vertex.site, vertex.part, transport,
1549 # local_vertex.is_black(), detect_failed)
1551 # TODO WHY?????
1553 vertex.accept_red_red = []
1554 vertex.accept_black = []
1555 found_failed = False
1557 if vertex in graph.connected_vertices:
1558 t_guid = str(self.ip_transport.guid)
1560 bh = self.get_bridgehead(vertex.site, vertex.part,
1561 self.ip_transport,
1562 vertex.is_black(), detect_failed)
1563 if bh is None:
1564 if vertex.site.is_rodc_site():
1565 vertex.accept_red_red.append(t_guid)
1566 else:
1567 found_failed = True
1568 else:
1569 vertex.accept_red_red.append(t_guid)
1570 vertex.accept_black.append(t_guid)
1572 # Add additional transport to ensure another run of Dijkstra
1573 vertex.accept_red_red.append("EDGE_TYPE_ALL")
1574 vertex.accept_black.append("EDGE_TYPE_ALL")
1576 return found_failed
1578 def create_connections(self, graph, part, detect_failed):
1579 """Create intersite NTDSConnections as needed by a partition
1581 Construct an NC replica graph for the NC identified by
1582 the given crossRef, then create any additional nTDSConnection
1583 objects required.
1585 :param graph: site graph.
1586 :param part: crossRef object for NC.
1587 :param detect_failed: True to detect failed DCs and route
1588 replication traffic around them, False to assume no DC
1589 has failed.
1591 Modifies self.kept_connections by adding any connections
1592 deemed to be "in use".
1594 :return: (all_connected, found_failed_dc)
1595 (all_connected) True if the resulting NC replica graph
1596 connects all sites that need to be connected.
1597 (found_failed_dc) True if one or more failed DCs were
1598 detected.
1600 all_connected = True
1601 found_failed = False
1603 DEBUG_FN("create_connections(): enter\n"
1604 "\tpartdn=%s\n\tdetect_failed=%s" %
1605 (part.nc_dnstr, detect_failed))
1607 # XXX - This is a highly abbreviated function from the MS-TECH
1608 # ref. It creates connections between bridgeheads to all
1609 # sites that have appropriate replicas. Thus we are not
1610 # creating a minimum cost spanning tree but instead
1611 # producing a fully connected tree. This should produce
1612 # a full (albeit not optimal cost) replication topology.
1614 my_vertex = Vertex(self.my_site, part)
1615 my_vertex.color_vertex()
1617 for v in graph.vertices:
1618 v.color_vertex()
1619 if self.add_transports(v, my_vertex, graph, detect_failed):
1620 found_failed = True
1622 # No NC replicas for this NC in the site of the local DC,
1623 # so no nTDSConnection objects need be created
1624 if my_vertex.is_white():
1625 return all_connected, found_failed
1627 edge_list, n_components = get_spanning_tree_edges(graph,
1628 self.my_site,
1629 label=part.partstr)
1631 DEBUG_FN("%s Number of components: %d" %
1632 (part.nc_dnstr, n_components))
1633 if n_components > 1:
1634 all_connected = False
1636 # LET partialReplicaOkay be TRUE if and only if
1637 # localSiteVertex.Color = COLOR.BLACK
1638 partial_ok = my_vertex.is_black()
1640 # Utilize the IP transport only for now
1641 transport = self.ip_transport
1643 DEBUG("edge_list %s" % edge_list)
1644 for e in edge_list:
1645 # XXX more accurate comparison?
1646 if e.directed and e.vertices[0].site is self.my_site:
1647 continue
1649 if e.vertices[0].site is self.my_site:
1650 rsite = e.vertices[1].site
1651 else:
1652 rsite = e.vertices[0].site
1654 # We don't make connections to our own site as that
1655 # is intrasite topology generator's job
1656 if rsite is self.my_site:
1657 DEBUG("rsite is my_site")
1658 continue
1660 # Determine bridgehead server in remote site
1661 rbh = self.get_bridgehead(rsite, part, transport,
1662 partial_ok, detect_failed)
1663 if rbh is None:
1664 continue
1666 # RODC acts as an BH for itself
1667 # IF AmIRODC() then
1668 # LET lbh be the nTDSDSA object of the local DC
1669 # ELSE
1670 # LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1671 # cr, t, partialReplicaOkay, detectFailedDCs)
1672 if self.my_dsa.is_ro():
1673 lsite = self.my_site
1674 lbh = self.my_dsa
1675 else:
1676 lsite = self.my_site
1677 lbh = self.get_bridgehead(lsite, part, transport,
1678 partial_ok, detect_failed)
1679 # TODO
1680 if lbh is None:
1681 debug.DEBUG_RED("DISASTER! lbh is None")
1682 return False, True
1684 DEBUG_FN("lsite: %s\nrsite: %s" % (lsite, rsite))
1685 DEBUG_FN("vertices %s" % (e.vertices,))
1686 debug.DEBUG_BLUE("bridgeheads\n%s\n%s\n%s" % (lbh, rbh, "-" * 70))
1688 sitelink = e.site_link
1689 if sitelink is None:
1690 link_opt = 0x0
1691 link_sched = None
1692 else:
1693 link_opt = sitelink.options
1694 link_sched = sitelink.schedule
1696 self.create_connection(part, rbh, rsite, transport,
1697 lbh, lsite, link_opt, link_sched,
1698 partial_ok, detect_failed)
1700 return all_connected, found_failed
1702 def create_intersite_connections(self):
1703 """Create NTDSConnections as necessary for all partitions.
1705 Computes an NC replica graph for each NC replica that "should be
1706 present" on the local DC or "is present" on any DC in the same site
1707 as the local DC. For each edge directed to an NC replica on such a
1708 DC from an NC replica on a DC in another site, the KCC creates an
1709 nTDSConnection object to imply that edge if one does not already
1710 exist.
1712 Modifies self.kept_connections - A set of nTDSConnection
1713 objects for edges that are directed
1714 to the local DC's site in one or more NC replica graphs.
1716 :return: True if spanning trees were created for all NC replica
1717 graphs, otherwise False.
1719 all_connected = True
1720 self.kept_connections = set()
1722 # LET crossRefList be the set containing each object o of class
1723 # crossRef such that o is a child of the CN=Partitions child of the
1724 # config NC
1726 # FOR each crossRef object cr in crossRefList
1727 # IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1728 # is clear in cr!systemFlags, skip cr.
1729 # LET g be the GRAPH return of SetupGraph()
1731 for part in self.part_table.values():
1733 if not part.is_enabled():
1734 continue
1736 if part.is_foreign():
1737 continue
1739 graph = self.setup_graph(part)
1741 # Create nTDSConnection objects, routing replication traffic
1742 # around "failed" DCs.
1743 found_failed = False
1745 connected, found_failed = self.create_connections(graph,
1746 part, True)
1748 DEBUG("with detect_failed: connected %s Found failed %s" %
1749 (connected, found_failed))
1750 if not connected:
1751 all_connected = False
1753 if found_failed:
1754 # One or more failed DCs preclude use of the ideal NC
1755 # replica graph. Add connections for the ideal graph.
1756 self.create_connections(graph, part, False)
1758 return all_connected
1760 def intersite(self, ping):
1761 """Generate the inter-site KCC replica graph and nTDSConnections
1763 As per MS-ADTS 6.2.2.3.
1765 If self.readonly is False, the connections are added to self.samdb.
1767 Produces self.kept_connections which is a set of NTDS
1768 Connections that should be kept during subsequent pruning
1769 process.
1771 After this has run, all sites should be connected in a minimum
1772 spanning tree.
1774 :param ping: An oracle function of remote site availability
1775 :return (True or False): (True) if the produced NC replica
1776 graph connects all sites that need to be connected
1779 # Retrieve my DSA
1780 mydsa = self.my_dsa
1781 mysite = self.my_site
1782 all_connected = True
1784 DEBUG_FN("intersite(): enter")
1786 # Determine who is the ISTG
1787 if self.readonly:
1788 mysite.select_istg(self.samdb, mydsa, ro=True)
1789 else:
1790 mysite.select_istg(self.samdb, mydsa, ro=False)
1792 # Test whether local site has topology disabled
1793 if mysite.is_intersite_topology_disabled():
1794 DEBUG_FN("intersite(): exit disabled all_connected=%d" %
1795 all_connected)
1796 return all_connected
1798 if not mydsa.is_istg():
1799 DEBUG_FN("intersite(): exit not istg all_connected=%d" %
1800 all_connected)
1801 return all_connected
1803 self.merge_failed_links(ping)
1805 # For each NC with an NC replica that "should be present" on the
1806 # local DC or "is present" on any DC in the same site as the
1807 # local DC, the KCC constructs a site graph--a precursor to an NC
1808 # replica graph. The site connectivity for a site graph is defined
1809 # by objects of class interSiteTransport, siteLink, and
1810 # siteLinkBridge in the config NC.
1812 all_connected = self.create_intersite_connections()
1814 DEBUG_FN("intersite(): exit all_connected=%d" % all_connected)
1815 return all_connected
1817 # This function currently does no actions. The reason being that we cannot
1818 # perform modifies in this way on the RODC.
1819 def update_rodc_connection(self, ro=True):
1820 """Updates the RODC NTFRS connection object.
1822 If the local DSA is not an RODC, this does nothing.
1824 if not self.my_dsa.is_ro():
1825 return
1827 # Given an nTDSConnection object cn1, such that cn1.options contains
1828 # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1829 # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1830 # that the following is true:
1832 # cn1.fromServer = cn2.fromServer
1833 # cn1.schedule = cn2.schedule
1835 # If no such cn2 can be found, cn1 is not modified.
1836 # If no such cn1 can be found, nothing is modified by this task.
1838 all_connections = self.my_dsa.connect_table.values()
1839 ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1840 rw_connections = [x for x in all_connections
1841 if x not in ro_connections]
1843 # XXX here we are dealing with multiple RODC_TOPO connections,
1844 # if they exist. It is not clear whether the spec means that
1845 # or if it ever arises.
1846 if rw_connections and ro_connections:
1847 for con in ro_connections:
1848 cn2 = rw_connections[0]
1849 con.from_dnstr = cn2.from_dnstr
1850 con.schedule = cn2.schedule
1851 con.to_be_modified = True
1853 self.my_dsa.commit_connections(self.samdb, ro=ro)
1855 def intrasite_max_node_edges(self, node_count):
1856 """Find the maximum number of edges directed to an intrasite node
1858 The KCC does not create more than 50 edges directed to a
1859 single DC. To optimize replication, we compute that each node
1860 should have n+2 total edges directed to it such that (n) is
1861 the smallest non-negative integer satisfying
1862 (node_count <= 2*(n*n) + 6*n + 7)
1864 (If the number of edges is m (i.e. n + 2), that is the same as
1865 2 * m*m - 2 * m + 3). We think in terms of n because that is
1866 the number of extra connections over the double directed ring
1867 that exists by default.
1869 edges n nodecount
1870 2 0 7
1871 3 1 15
1872 4 2 27
1873 5 3 43
1875 50 48 4903
1877 :param node_count: total number of nodes in the replica graph
1879 The intention is that there should be no more than 3 hops
1880 between any two DSAs at a site. With up to 7 nodes the 2 edges
1881 of the ring are enough; any configuration of extra edges with
1882 8 nodes will be enough. It is less clear that the 3 hop
1883 guarantee holds at e.g. 15 nodes in degenerate cases, but
1884 those are quite unlikely given the extra edges are randomly
1885 arranged.
1887 :param node_count: the number of nodes in the site
1888 "return: The desired maximum number of connections
1890 n = 0
1891 while True:
1892 if node_count <= (2 * (n * n) + (6 * n) + 7):
1893 break
1894 n = n + 1
1895 n = n + 2
1896 if n < 50:
1897 return n
1898 return 50
1900 def construct_intrasite_graph(self, site_local, dc_local,
1901 nc_x, gc_only, detect_stale):
1902 """Create an intrasite graph using given parameters
1904 This might be called a number of times per site with different
1905 parameters.
1907 Based on [MS-ADTS] 6.2.2.2
1909 :param site_local: site for which we are working
1910 :param dc_local: local DC that potentially needs a replica
1911 :param nc_x: naming context (x) that we are testing if it
1912 "should be present" on the local DC
1913 :param gc_only: Boolean - only consider global catalog servers
1914 :param detect_stale: Boolean - check whether links seems down
1915 :return: None
1917 # We're using the MS notation names here to allow
1918 # correlation back to the published algorithm.
1920 # nc_x - naming context (x) that we are testing if it
1921 # "should be present" on the local DC
1922 # f_of_x - replica (f) found on a DC (s) for NC (x)
1923 # dc_s - DC where f_of_x replica was found
1924 # dc_local - local DC that potentially needs a replica
1925 # (f_of_x)
1926 # r_list - replica list R
1927 # p_of_x - replica (p) is partial and found on a DC (s)
1928 # for NC (x)
1929 # l_of_x - replica (l) is the local replica for NC (x)
1930 # that should appear on the local DC
1931 # r_len = is length of replica list |R|
1933 # If the DSA doesn't need a replica for this
1934 # partition (NC x) then continue
1935 needed, ro, partial = nc_x.should_be_present(dc_local)
1937 debug.DEBUG_YELLOW("construct_intrasite_graph(): enter" +
1938 "\n\tgc_only=%d" % gc_only +
1939 "\n\tdetect_stale=%d" % detect_stale +
1940 "\n\tneeded=%s" % needed +
1941 "\n\tro=%s" % ro +
1942 "\n\tpartial=%s" % partial +
1943 "\n%s" % nc_x)
1945 if not needed:
1946 debug.DEBUG_RED("%s lacks 'should be present' status, "
1947 "aborting construct_intersite_graph!" %
1948 nc_x.nc_dnstr)
1949 return
1951 # Create a NCReplica that matches what the local replica
1952 # should say. We'll use this below in our r_list
1953 l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid,
1954 nc_x.nc_dnstr)
1956 l_of_x.identify_by_basedn(self.samdb)
1958 l_of_x.rep_partial = partial
1959 l_of_x.rep_ro = ro
1961 # Add this replica that "should be present" to the
1962 # needed replica table for this DSA
1963 dc_local.add_needed_replica(l_of_x)
1965 # Replica list
1967 # Let R be a sequence containing each writable replica f of x
1968 # such that f "is present" on a DC s satisfying the following
1969 # criteria:
1971 # * s is a writable DC other than the local DC.
1973 # * s is in the same site as the local DC.
1975 # * If x is a read-only full replica and x is a domain NC,
1976 # then the DC's functional level is at least
1977 # DS_BEHAVIOR_WIN2008.
1979 # * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
1980 # in the options attribute of the site settings object for
1981 # the local DC's site, or no tuple z exists in the
1982 # kCCFailedLinks or kCCFailedConnections variables such
1983 # that z.UUIDDsa is the objectGUID of the nTDSDSA object
1984 # for s, z.FailureCount > 0, and the current time -
1985 # z.TimeFirstFailure > 2 hours.
1987 r_list = []
1989 # We'll loop thru all the DSAs looking for
1990 # writeable NC replicas that match the naming
1991 # context dn for (nc_x)
1993 for dc_s in self.my_site.dsa_table.values():
1994 # If this partition (nc_x) doesn't appear as a
1995 # replica (f_of_x) on (dc_s) then continue
1996 if not nc_x.nc_dnstr in dc_s.current_rep_table:
1997 continue
1999 # Pull out the NCReplica (f) of (x) with the dn
2000 # that matches NC (x) we are examining.
2001 f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2003 # Replica (f) of NC (x) must be writable
2004 if f_of_x.is_ro():
2005 continue
2007 # Replica (f) of NC (x) must satisfy the
2008 # "is present" criteria for DC (s) that
2009 # it was found on
2010 if not f_of_x.is_present():
2011 continue
2013 # DC (s) must be a writable DSA other than
2014 # my local DC. In other words we'd only replicate
2015 # from other writable DC
2016 if dc_s.is_ro() or dc_s is dc_local:
2017 continue
2019 # Certain replica graphs are produced only
2020 # for global catalogs, so test against
2021 # method input parameter
2022 if gc_only and not dc_s.is_gc():
2023 continue
2025 # DC (s) must be in the same site as the local DC
2026 # as this is the intra-site algorithm. This is
2027 # handled by virtue of placing DSAs in per
2028 # site objects (see enclosing for() loop)
2030 # If NC (x) is intended to be read-only full replica
2031 # for a domain NC on the target DC then the source
2032 # DC should have functional level at minimum WIN2008
2034 # Effectively we're saying that in order to replicate
2035 # to a targeted RODC (which was introduced in Windows 2008)
2036 # then we have to replicate from a DC that is also minimally
2037 # at that level.
2039 # You can also see this requirement in the MS special
2040 # considerations for RODC which state that to deploy
2041 # an RODC, at least one writable domain controller in
2042 # the domain must be running Windows Server 2008
2043 if ro and not partial and nc_x.nc_type == NCType.domain:
2044 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2045 continue
2047 # If we haven't been told to turn off stale connection
2048 # detection and this dsa has a stale connection then
2049 # continue
2050 if detect_stale and self.is_stale_link_connection(dc_s):
2051 continue
2053 # Replica meets criteria. Add it to table indexed
2054 # by the GUID of the DC that it appears on
2055 r_list.append(f_of_x)
2057 # If a partial (not full) replica of NC (x) "should be present"
2058 # on the local DC, append to R each partial replica (p of x)
2059 # such that p "is present" on a DC satisfying the same
2060 # criteria defined above for full replica DCs.
2062 # XXX This loop and the previous one differ only in whether
2063 # the replica is partial or not. here we only accept partial
2064 # (because we're partial); before we only accepted full. Order
2065 # doen't matter (the list is sorted a few lines down) so these
2066 # loops could easily be merged. Or this could be a helper
2067 # function.
2069 if partial:
2070 # Now we loop thru all the DSAs looking for
2071 # partial NC replicas that match the naming
2072 # context dn for (NC x)
2073 for dc_s in self.my_site.dsa_table.values():
2075 # If this partition NC (x) doesn't appear as a
2076 # replica (p) of NC (x) on the dsa DC (s) then
2077 # continue
2078 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2079 continue
2081 # Pull out the NCReplica with the dn that
2082 # matches NC (x) we are examining.
2083 p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2085 # Replica (p) of NC (x) must be partial
2086 if not p_of_x.is_partial():
2087 continue
2089 # Replica (p) of NC (x) must satisfy the
2090 # "is present" criteria for DC (s) that
2091 # it was found on
2092 if not p_of_x.is_present():
2093 continue
2095 # DC (s) must be a writable DSA other than
2096 # my DSA. In other words we'd only replicate
2097 # from other writable DSA
2098 if dc_s.is_ro() or dc_s is dc_local:
2099 continue
2101 # Certain replica graphs are produced only
2102 # for global catalogs, so test against
2103 # method input parameter
2104 if gc_only and not dc_s.is_gc():
2105 continue
2107 # If we haven't been told to turn off stale connection
2108 # detection and this dsa has a stale connection then
2109 # continue
2110 if detect_stale and self.is_stale_link_connection(dc_s):
2111 continue
2113 # Replica meets criteria. Add it to table indexed
2114 # by the GUID of the DSA that it appears on
2115 r_list.append(p_of_x)
2117 # Append to R the NC replica that "should be present"
2118 # on the local DC
2119 r_list.append(l_of_x)
2121 r_list.sort(sort_replica_by_dsa_guid)
2122 r_len = len(r_list)
2124 max_node_edges = self.intrasite_max_node_edges(r_len)
2126 # Add a node for each r_list element to the replica graph
2127 graph_list = []
2128 for rep in r_list:
2129 node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2130 graph_list.append(node)
2132 # For each r(i) from (0 <= i < |R|-1)
2133 i = 0
2134 while i < (r_len-1):
2135 # Add an edge from r(i) to r(i+1) if r(i) is a full
2136 # replica or r(i+1) is a partial replica
2137 if not r_list[i].is_partial() or r_list[i+1].is_partial():
2138 graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
2140 # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2141 # replica or ri is a partial replica.
2142 if not r_list[i+1].is_partial() or r_list[i].is_partial():
2143 graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
2144 i = i + 1
2146 # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2147 # or r0 is a partial replica.
2148 if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
2149 graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
2151 # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2152 # r|R|-1 is a partial replica.
2153 if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
2154 graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
2156 DEBUG("r_list is length %s" % len(r_list))
2157 DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr))
2158 for x in r_list))
2160 do_dot_files = self.dot_file_dir is not None and self.debug
2161 if self.verify or do_dot_files:
2162 dot_edges = []
2163 dot_vertices = set()
2164 for v1 in graph_list:
2165 dot_vertices.add(v1.dsa_dnstr)
2166 for v2 in v1.edge_from:
2167 dot_edges.append((v2, v1.dsa_dnstr))
2168 dot_vertices.add(v2)
2170 verify_properties = ('connected',)
2171 verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2172 label='%s__%s__%s' % (site_local.site_dnstr,
2173 nctype_lut[nc_x.nc_type],
2174 nc_x.nc_dnstr),
2175 properties=verify_properties, debug=DEBUG,
2176 verify=self.verify,
2177 dot_file_dir=self.dot_file_dir,
2178 directed=True)
2180 rw_dot_vertices = set(x for x in dot_vertices
2181 if not self.get_dsa(x).is_ro())
2182 rw_dot_edges = [(a, b) for a, b in dot_edges if
2183 a in rw_dot_vertices and b in rw_dot_vertices]
2184 rw_verify_properties = ('connected',
2185 'directed_double_ring_or_small')
2186 verify_and_dot('intrasite_rw_pre_ntdscon', rw_dot_edges,
2187 rw_dot_vertices,
2188 label='%s__%s__%s' % (site_local.site_dnstr,
2189 nctype_lut[nc_x.nc_type],
2190 nc_x.nc_dnstr),
2191 properties=rw_verify_properties, debug=DEBUG,
2192 verify=self.verify,
2193 dot_file_dir=self.dot_file_dir,
2194 directed=True)
2196 # For each existing nTDSConnection object implying an edge
2197 # from rj of R to ri such that j != i, an edge from rj to ri
2198 # is not already in the graph, and the total edges directed
2199 # to ri is less than n+2, the KCC adds that edge to the graph.
2200 for vertex in graph_list:
2201 dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2202 for connect in dsa.connect_table.values():
2203 remote = connect.from_dnstr
2204 if remote in self.my_site.dsa_table:
2205 vertex.add_edge_from(remote)
2207 DEBUG('reps are: %s' % ' '.join(x.rep_dsa_dnstr for x in r_list))
2208 DEBUG('dsas are: %s' % ' '.join(x.dsa_dnstr for x in graph_list))
2210 for tnode in graph_list:
2211 # To optimize replication latency in sites with many NC
2212 # replicas, the KCC adds new edges directed to ri to bring
2213 # the total edges to n+2, where the NC replica rk of R
2214 # from which the edge is directed is chosen at random such
2215 # that k != i and an edge from rk to ri is not already in
2216 # the graph.
2218 # Note that the KCC tech ref does not give a number for
2219 # the definition of "sites with many NC replicas". At a
2220 # bare minimum to satisfy n+2 edges directed at a node we
2221 # have to have at least three replicas in |R| (i.e. if n
2222 # is zero then at least replicas from two other graph
2223 # nodes may direct edges to us).
2224 if r_len >= 3 and not tnode.has_sufficient_edges():
2225 candidates = [x for x in graph_list if
2226 (x is not tnode and
2227 x.dsa_dnstr not in tnode.edge_from)]
2229 debug.DEBUG_BLUE("looking for random link for %s. r_len %d, "
2230 "graph len %d candidates %d"
2231 % (tnode.dsa_dnstr, r_len, len(graph_list),
2232 len(candidates)))
2234 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2236 while candidates and not tnode.has_sufficient_edges():
2237 other = random.choice(candidates)
2238 DEBUG("trying to add candidate %s" % other.dsa_dnstr)
2239 if not tnode.add_edge_from(other):
2240 debug.DEBUG_RED("could not add %s" % other.dsa_dnstr)
2241 candidates.remove(other)
2242 else:
2243 DEBUG_FN("not adding links to %s: nodes %s, links is %s/%s" %
2244 (tnode.dsa_dnstr, r_len, len(tnode.edge_from),
2245 tnode.max_edges))
2247 # Print the graph node in debug mode
2248 DEBUG_FN("%s" % tnode)
2250 # For each edge directed to the local DC, ensure a nTDSConnection
2251 # points to us that satisfies the KCC criteria
2253 if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2254 tnode.add_connections_from_edges(dc_local, self.ip_transport)
2256 if self.verify or do_dot_files:
2257 dot_edges = []
2258 dot_vertices = set()
2259 for v1 in graph_list:
2260 dot_vertices.add(v1.dsa_dnstr)
2261 for v2 in v1.edge_from:
2262 dot_edges.append((v2, v1.dsa_dnstr))
2263 dot_vertices.add(v2)
2265 verify_properties = ('connected',)
2266 verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2267 label='%s__%s__%s' % (site_local.site_dnstr,
2268 nctype_lut[nc_x.nc_type],
2269 nc_x.nc_dnstr),
2270 properties=verify_properties, debug=DEBUG,
2271 verify=self.verify,
2272 dot_file_dir=self.dot_file_dir,
2273 directed=True)
2275 rw_dot_vertices = set(x for x in dot_vertices
2276 if not self.get_dsa(x).is_ro())
2277 rw_dot_edges = [(a, b) for a, b in dot_edges if
2278 a in rw_dot_vertices and b in rw_dot_vertices]
2279 rw_verify_properties = ('connected',
2280 'directed_double_ring_or_small')
2281 verify_and_dot('intrasite_rw_post_ntdscon', rw_dot_edges,
2282 rw_dot_vertices,
2283 label='%s__%s__%s' % (site_local.site_dnstr,
2284 nctype_lut[nc_x.nc_type],
2285 nc_x.nc_dnstr),
2286 properties=rw_verify_properties, debug=DEBUG,
2287 verify=self.verify,
2288 dot_file_dir=self.dot_file_dir,
2289 directed=True)
2291 def intrasite(self):
2292 """Generate the intrasite KCC connections
2294 As per MS-ADTS 6.2.2.2.
2296 If self.readonly is False, the connections are added to self.samdb.
2298 After this call, all DCs in each site with more than 3 DCs
2299 should be connected in a bidirectional ring. If a site has 2
2300 DCs, they will bidirectionally connected. Sites with many DCs
2301 may have arbitrary extra connections.
2303 :return: None
2305 mydsa = self.my_dsa
2307 DEBUG_FN("intrasite(): enter")
2309 # Test whether local site has topology disabled
2310 mysite = self.my_site
2311 if mysite.is_intrasite_topology_disabled():
2312 return
2314 detect_stale = (not mysite.is_detect_stale_disabled())
2315 for connect in mydsa.connect_table.values():
2316 if connect.to_be_added:
2317 debug.DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2319 # Loop thru all the partitions, with gc_only False
2320 for partdn, part in self.part_table.items():
2321 self.construct_intrasite_graph(mysite, mydsa, part, False,
2322 detect_stale)
2323 for connect in mydsa.connect_table.values():
2324 if connect.to_be_added:
2325 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2327 # If the DC is a GC server, the KCC constructs an additional NC
2328 # replica graph (and creates nTDSConnection objects) for the
2329 # config NC as above, except that only NC replicas that "are present"
2330 # on GC servers are added to R.
2331 for connect in mydsa.connect_table.values():
2332 if connect.to_be_added:
2333 debug.DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2335 # Do it again, with gc_only True
2336 for partdn, part in self.part_table.items():
2337 if part.is_config():
2338 self.construct_intrasite_graph(mysite, mydsa, part, True,
2339 detect_stale)
2341 # The DC repeats the NC replica graph computation and nTDSConnection
2342 # creation for each of the NC replica graphs, this time assuming
2343 # that no DC has failed. It does so by re-executing the steps as
2344 # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2345 # set in the options attribute of the site settings object for
2346 # the local DC's site. (ie. we set "detec_stale" flag to False)
2347 for connect in mydsa.connect_table.values():
2348 if connect.to_be_added:
2349 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2351 # Loop thru all the partitions.
2352 for partdn, part in self.part_table.items():
2353 self.construct_intrasite_graph(mysite, mydsa, part, False,
2354 False) # don't detect stale
2356 # If the DC is a GC server, the KCC constructs an additional NC
2357 # replica graph (and creates nTDSConnection objects) for the
2358 # config NC as above, except that only NC replicas that "are present"
2359 # on GC servers are added to R.
2360 for connect in mydsa.connect_table.values():
2361 if connect.to_be_added:
2362 debug.DEBUG_RED("TO BE ADDED:\n%s" % connect)
2364 for partdn, part in self.part_table.items():
2365 if part.is_config():
2366 self.construct_intrasite_graph(mysite, mydsa, part, True,
2367 False) # don't detect stale
2369 self._commit_changes(mydsa)
2371 def list_dsas(self):
2372 """Compile a comprehensive list of DSA DNs
2374 These are all the DSAs on all the sites that KCC would be
2375 dealing with.
2377 This method is not idempotent and may not work correctly in
2378 sequence with KCC.run().
2380 :return: a list of DSA DN strings.
2382 self.load_my_site()
2383 self.load_my_dsa()
2385 self.load_all_sites()
2386 self.load_all_partitions()
2387 self.load_ip_transport()
2388 self.load_all_sitelinks()
2389 dsas = []
2390 for site in self.site_table.values():
2391 dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2392 for dsa in site.dsa_table.values()])
2393 return dsas
2395 def load_samdb(self, dburl, lp, creds, force=False):
2396 """Load the database using an url, loadparm, and credentials
2398 If force is False, the samdb won't be reloaded if it already
2399 exists.
2401 :param dburl: a database url.
2402 :param lp: a loadparm object.
2403 :param creds: a Credentials object.
2404 :param force: a boolean indicating whether to overwrite.
2407 if force or self.samdb is None:
2408 try:
2409 self.samdb = SamDB(url=dburl,
2410 session_info=system_session(),
2411 credentials=creds, lp=lp)
2412 except ldb.LdbError, (num, msg):
2413 raise KCCError("Unable to open sam database %s : %s" %
2414 (dburl, msg))
2416 def plot_all_connections(self, basename, verify_properties=()):
2417 """Helper function to plot and verify NTDSConnections
2419 :param basename: an identifying string to use in filenames and logs.
2420 :param verify_properties: properties to verify (default empty)
2422 verify = verify_properties and self.verify
2423 if not verify and self.dot_file_dir is None:
2424 return
2426 dot_edges = []
2427 dot_vertices = []
2428 edge_colours = []
2429 vertex_colours = []
2431 for dsa in self.dsa_by_dnstr.values():
2432 dot_vertices.append(dsa.dsa_dnstr)
2433 if dsa.is_ro():
2434 vertex_colours.append('#cc0000')
2435 else:
2436 vertex_colours.append('#0000cc')
2437 for con in dsa.connect_table.values():
2438 if con.is_rodc_topology():
2439 edge_colours.append('red')
2440 else:
2441 edge_colours.append('blue')
2442 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2444 verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2445 label=self.my_dsa_dnstr,
2446 properties=verify_properties, debug=DEBUG,
2447 verify=verify, dot_file_dir=self.dot_file_dir,
2448 directed=True, edge_colors=edge_colours,
2449 vertex_colors=vertex_colours)
2451 def run(self, dburl, lp, creds, forced_local_dsa=None,
2452 forget_local_links=False, forget_intersite_links=False,
2453 attempt_live_connections=False):
2454 """Perform a KCC run, possibly updating repsFrom topology
2456 :param dburl: url of the database to work with.
2457 :param lp: a loadparm object.
2458 :param creds: a Credentials object.
2459 :param forced_local_dsa: pretend to be on the DSA with this dn_str
2460 :param forget_local_links: calculate as if no connections existed
2461 (boolean, default False)
2462 :param forget_intersite_links: calculate with only intrasite connection
2463 (boolean, default False)
2464 :param attempt_live_connections: attempt to connect to remote DSAs to
2465 determine link availability (boolean, default False)
2466 :return: 1 on error, 0 otherwise
2468 if self.samdb is None:
2469 DEBUG_FN("samdb is None; let's load it from %s" % (dburl,))
2470 self.load_samdb(dburl, lp, creds, force=False)
2472 if forced_local_dsa:
2473 self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" %
2474 forced_local_dsa)
2476 try:
2477 # Setup
2478 self.load_my_site()
2479 self.load_my_dsa()
2481 self.load_all_sites()
2482 self.load_all_partitions()
2483 self.load_ip_transport()
2484 self.load_all_sitelinks()
2486 if self.verify or self.dot_file_dir is not None:
2487 guid_to_dnstr = {}
2488 for site in self.site_table.values():
2489 guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2490 for dnstr, dsa
2491 in site.dsa_table.items())
2493 self.plot_all_connections('dsa_initial')
2495 dot_edges = []
2496 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2497 for dnstr, c_rep in current_reps.items():
2498 DEBUG("c_rep %s" % c_rep)
2499 dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2501 verify_and_dot('dsa_repsFrom_initial', dot_edges,
2502 directed=True, label=self.my_dsa_dnstr,
2503 properties=(), debug=DEBUG, verify=self.verify,
2504 dot_file_dir=self.dot_file_dir)
2506 dot_edges = []
2507 for site in self.site_table.values():
2508 for dsa in site.dsa_table.values():
2509 current_reps, needed_reps = dsa.get_rep_tables()
2510 for dn_str, rep in current_reps.items():
2511 for reps_from in rep.rep_repsFrom:
2512 DEBUG("rep %s" % rep)
2513 dsa_guid = str(reps_from.source_dsa_obj_guid)
2514 dsa_dn = guid_to_dnstr[dsa_guid]
2515 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2517 verify_and_dot('dsa_repsFrom_initial_all', dot_edges,
2518 directed=True, label=self.my_dsa_dnstr,
2519 properties=(), debug=DEBUG, verify=self.verify,
2520 dot_file_dir=self.dot_file_dir)
2522 dot_edges = []
2523 for link in self.sitelink_table.values():
2524 for a, b in itertools.combinations(link.site_list, 2):
2525 dot_edges.append((str(a), str(b)))
2526 properties = ('connected',)
2527 verify_and_dot('dsa_sitelink_initial', dot_edges,
2528 directed=False,
2529 label=self.my_dsa_dnstr, properties=properties,
2530 debug=DEBUG, verify=self.verify,
2531 dot_file_dir=self.dot_file_dir)
2533 if forget_local_links:
2534 for dsa in self.my_site.dsa_table.values():
2535 dsa.connect_table = dict((k, v) for k, v in
2536 dsa.connect_table.items()
2537 if v.is_rodc_topology() or
2538 (v.from_dnstr not in
2539 self.my_site.dsa_table))
2540 self.plot_all_connections('dsa_forgotten_local')
2542 if forget_intersite_links:
2543 for site in self.site_table.values():
2544 for dsa in site.dsa_table.values():
2545 dsa.connect_table = dict((k, v) for k, v in
2546 dsa.connect_table.items()
2547 if site is self.my_site and
2548 v.is_rodc_topology())
2550 self.plot_all_connections('dsa_forgotten_all')
2552 if attempt_live_connections:
2553 # Encapsulates lp and creds in a function that
2554 # attempts connections to remote DSAs.
2555 def ping(self, dnsname):
2556 try:
2557 drs_utils.drsuapi_connect(dnsname, self.lp, self.creds)
2558 except drs_utils.drsException:
2559 return False
2560 return True
2561 else:
2562 ping = None
2563 # These are the published steps (in order) for the
2564 # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2566 # Step 1
2567 self.refresh_failed_links_connections(ping)
2569 # Step 2
2570 self.intrasite()
2572 # Step 3
2573 all_connected = self.intersite(ping)
2575 # Step 4
2576 self.remove_unneeded_ntdsconn(all_connected)
2578 # Step 5
2579 self.translate_ntdsconn()
2581 # Step 6
2582 self.remove_unneeded_failed_links_connections()
2584 # Step 7
2585 self.update_rodc_connection()
2587 if self.verify or self.dot_file_dir is not None:
2588 self.plot_all_connections('dsa_final',
2589 ('connected',))
2591 debug.DEBUG_MAGENTA("there are %d dsa guids" %
2592 len(guid_to_dnstr))
2594 dot_edges = []
2595 edge_colors = []
2596 my_dnstr = self.my_dsa.dsa_dnstr
2597 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2598 for dnstr, n_rep in needed_reps.items():
2599 for reps_from in n_rep.rep_repsFrom:
2600 guid_str = str(reps_from.source_dsa_obj_guid)
2601 dot_edges.append((my_dnstr, guid_to_dnstr[guid_str]))
2602 edge_colors.append('#' + str(n_rep.nc_guid)[:6])
2604 verify_and_dot('dsa_repsFrom_final', dot_edges, directed=True,
2605 label=self.my_dsa_dnstr,
2606 properties=(), debug=DEBUG, verify=self.verify,
2607 dot_file_dir=self.dot_file_dir,
2608 edge_colors=edge_colors)
2610 dot_edges = []
2612 for site in self.site_table.values():
2613 for dsa in site.dsa_table.values():
2614 current_reps, needed_reps = dsa.get_rep_tables()
2615 for n_rep in needed_reps.values():
2616 for reps_from in n_rep.rep_repsFrom:
2617 dsa_guid = str(reps_from.source_dsa_obj_guid)
2618 dsa_dn = guid_to_dnstr[dsa_guid]
2619 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2621 verify_and_dot('dsa_repsFrom_final_all', dot_edges,
2622 directed=True, label=self.my_dsa_dnstr,
2623 properties=(), debug=DEBUG, verify=self.verify,
2624 dot_file_dir=self.dot_file_dir)
2626 except:
2627 raise
2629 return 0
2631 def import_ldif(self, dburl, lp, ldif_file, forced_local_dsa=None):
2632 """Import relevant objects and attributes from an LDIF file.
2634 The point of this function is to allow a programmer/debugger to
2635 import an LDIF file with non-security relevent information that
2636 was previously extracted from a DC database. The LDIF file is used
2637 to create a temporary abbreviated database. The KCC algorithm can
2638 then run against this abbreviated database for debug or test
2639 verification that the topology generated is computationally the
2640 same between different OSes and algorithms.
2642 :param dburl: path to the temporary abbreviated db to create
2643 :param lp: a loadparm object.
2644 :param ldif_file: path to the ldif file to import
2645 :param forced_local_dsa: perform KCC from this DSA's point of view
2646 :return: zero on success, 1 on error
2648 try:
2649 self.samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif_file,
2650 forced_local_dsa)
2651 except ldif_import_export.LdifError, e:
2652 logger.critical(e)
2653 return 1
2654 return 0
2656 def export_ldif(self, dburl, lp, creds, ldif_file):
2657 """Save KCC relevant details to an ldif file
2659 The point of this function is to allow a programmer/debugger to
2660 extract an LDIF file with non-security relevent information from
2661 a DC database. The LDIF file can then be used to "import" via
2662 the import_ldif() function this file into a temporary abbreviated
2663 database. The KCC algorithm can then run against this abbreviated
2664 database for debug or test verification that the topology generated
2665 is computationally the same between different OSes and algorithms.
2667 :param dburl: LDAP database URL to extract info from
2668 :param lp: a loadparm object.
2669 :param cred: a Credentials object.
2670 :param ldif_file: output LDIF file name to create
2671 :return: zero on success, 1 on error
2673 try:
2674 ldif_import_export.samdb_to_ldif_file(self.samdb, dburl, lp, creds,
2675 ldif_file)
2676 except ldif_import_export.LdifError, e:
2677 logger.critical(e)
2678 return 1
2679 return 0