KCC: Share commit wrapper between forget_ntdsconn and intrasite
[Samba.git] / python / samba / kcc / __init__.py
bloba3c31fb672d9dc2e5d5f454a25c974fa3f914521
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.info("Samba KCC is ignoring the obsolete SMTP transport.")
170 else:
171 logger.warning("Samba KCC does not support the transport called %r."
172 % (transport.name,))
174 if self.ip_transport is None:
175 raise KCCError("there doesn't seem to be an IP transport")
177 def load_all_sitelinks(self):
178 """Loads the inter-site siteLink objects
180 :return: None
181 :raise KCCError: if site-links aren't found
183 try:
184 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
185 self.samdb.get_config_basedn(),
186 scope=ldb.SCOPE_SUBTREE,
187 expression="(objectClass=siteLink)")
188 except ldb.LdbError, (enum, estr):
189 raise KCCError("Unable to find inter-site siteLinks - (%s)" % estr)
191 for msg in res:
192 dnstr = str(msg.dn)
194 # already loaded
195 if dnstr in self.sitelink_table:
196 continue
198 sitelink = SiteLink(dnstr)
200 sitelink.load_sitelink(self.samdb)
202 # Assign this siteLink to table
203 # and index by dn
204 self.sitelink_table[dnstr] = sitelink
206 def load_site(self, dn_str):
207 """Helper for load_my_site and load_all_sites.
209 Put all the site's DSAs into the KCC indices.
211 :param dn_str: a site dn_str
212 :return: the Site object pertaining to the dn_str
214 site = Site(dn_str, self.unix_now)
215 site.load_site(self.samdb)
217 # We avoid replacing the site with an identical copy in case
218 # somewhere else has a reference to the old one, which would
219 # lead to all manner of confusion and chaos.
220 guid = str(site.site_guid)
221 if guid not in self.site_table:
222 self.site_table[guid] = site
223 self.dsa_by_dnstr.update(site.dsa_table)
224 self.dsa_by_guid.update((str(x.dsa_guid), x)
225 for x in site.dsa_table.values())
227 return self.site_table[guid]
229 def load_my_site(self):
230 """Load the Site object for the local DSA.
232 :return: None
234 self.my_site_dnstr = ("CN=%s,CN=Sites,%s" % (
235 self.samdb.server_site_name(),
236 self.samdb.get_config_basedn()))
238 self.my_site = self.load_site(self.my_site_dnstr)
240 def load_all_sites(self):
241 """Discover all sites and create Site objects.
243 :return: None
244 :raise: KCCError if sites can't be found
246 try:
247 res = self.samdb.search("CN=Sites,%s" %
248 self.samdb.get_config_basedn(),
249 scope=ldb.SCOPE_SUBTREE,
250 expression="(objectClass=site)")
251 except ldb.LdbError, (enum, estr):
252 raise KCCError("Unable to find sites - (%s)" % estr)
254 for msg in res:
255 sitestr = str(msg.dn)
256 self.load_site(sitestr)
258 def load_my_dsa(self):
259 """Discover my nTDSDSA dn thru the rootDSE entry
261 :return: None
262 :raise: KCCError if DSA can't be found
264 dn = ldb.Dn(self.samdb, "<GUID=%s>" % self.samdb.get_ntds_GUID())
265 try:
266 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
267 attrs=["objectGUID"])
268 except ldb.LdbError, (enum, estr):
269 logger.warning("Search for %s failed: %s. This typically happens"
270 " in --importldif mode due to lack of module"
271 " support.", dn, estr)
272 try:
273 # We work around the failure above by looking at the
274 # dsServiceName that was put in the fake rootdse by
275 # the --exportldif, rather than the
276 # samdb.get_ntds_GUID(). The disadvantage is that this
277 # mode requires we modify the @ROOTDSE dnq to support
278 # --forced-local-dsa
279 service_name_res = self.samdb.search(base="",
280 scope=ldb.SCOPE_BASE,
281 attrs=["dsServiceName"])
282 dn = ldb.Dn(self.samdb,
283 service_name_res[0]["dsServiceName"][0])
285 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
286 attrs=["objectGUID"])
287 except ldb.LdbError, (enum, estr):
288 raise KCCError("Unable to find my nTDSDSA - (%s)" % estr)
290 if len(res) != 1:
291 raise KCCError("Unable to find my nTDSDSA at %s" %
292 dn.extended_str())
294 ntds_guid = misc.GUID(self.samdb.get_ntds_GUID())
295 if misc.GUID(res[0]["objectGUID"][0]) != ntds_guid:
296 raise KCCError("Did not find the GUID we expected,"
297 " perhaps due to --importldif")
299 self.my_dsa_dnstr = str(res[0].dn)
301 self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
303 if self.my_dsa_dnstr not in self.dsa_by_dnstr:
304 debug.DEBUG_DARK_YELLOW("my_dsa %s isn't in self.dsas_by_dnstr:"
305 " it must be RODC.\n"
306 "Let's add it, because my_dsa is special!"
307 "\n(likewise for self.dsa_by_guid)" %
308 self.my_dsas_dnstr)
310 self.dsa_by_dnstr[self.my_dsa_dnstr] = self.my_dsa
311 self.dsa_by_guid[str(self.my_dsa.dsa_guid)] = self.my_dsa
313 def load_all_partitions(self):
314 """Discover and load all partitions.
316 Each NC is inserted into the part_table by partition
317 dn string (not the nCName dn string)
319 :return: None
320 :raise: KCCError if partitions can't be found
322 try:
323 res = self.samdb.search("CN=Partitions,%s" %
324 self.samdb.get_config_basedn(),
325 scope=ldb.SCOPE_SUBTREE,
326 expression="(objectClass=crossRef)")
327 except ldb.LdbError, (enum, estr):
328 raise KCCError("Unable to find partitions - (%s)" % estr)
330 for msg in res:
331 partstr = str(msg.dn)
333 # already loaded
334 if partstr in self.part_table:
335 continue
337 part = Partition(partstr)
339 part.load_partition(self.samdb)
340 self.part_table[partstr] = part
342 def refresh_failed_links_connections(self, ping=None):
343 """Ensure the failed links list is up to date
345 Based on MS-ADTS 6.2.2.1
347 :param ping: An oracle function of remote site availability
348 :return: None
350 # LINKS: Refresh failed links
351 self.kcc_failed_links = {}
352 current, needed = self.my_dsa.get_rep_tables()
353 for replica in current.values():
354 # For every possible connection to replicate
355 for reps_from in replica.rep_repsFrom:
356 failure_count = reps_from.consecutive_sync_failures
357 if failure_count <= 0:
358 continue
360 dsa_guid = str(reps_from.source_dsa_obj_guid)
361 time_first_failure = reps_from.last_success
362 last_result = reps_from.last_attempt
363 dns_name = reps_from.dns_name1
365 f = self.kcc_failed_links.get(dsa_guid)
366 if f is None:
367 f = KCCFailedObject(dsa_guid, failure_count,
368 time_first_failure, last_result,
369 dns_name)
370 self.kcc_failed_links[dsa_guid] = f
371 else:
372 f.failure_count = max(f.failure_count, failure_count)
373 f.time_first_failure = min(f.time_first_failure,
374 time_first_failure)
375 f.last_result = last_result
377 # CONNECTIONS: Refresh failed connections
378 restore_connections = set()
379 if ping is not None:
380 DEBUG("refresh_failed_links: checking if links are still down")
381 for connection in self.kcc_failed_connections:
382 if ping(connection.dns_name):
383 # Failed connection is no longer failing
384 restore_connections.add(connection)
385 else:
386 connection.failure_count += 1
387 else:
388 DEBUG("refresh_failed_links: not checking live links because we\n"
389 "weren't asked to --attempt-live-connections")
391 # Remove the restored connections from the failed connections
392 self.kcc_failed_connections.difference_update(restore_connections)
394 def is_stale_link_connection(self, target_dsa):
395 """Check whether a link to a remote DSA is stale
397 Used in MS-ADTS 6.2.2.2 Intrasite Connection Creation
399 Returns True if the remote seems to have been down for at
400 least two hours, otherwise False.
402 :param target_dsa: the remote DSA object
403 :return: True if link is stale, otherwise False
405 failed_link = self.kcc_failed_links.get(str(target_dsa.dsa_guid))
406 if failed_link:
407 # failure_count should be > 0, but check anyways
408 if failed_link.failure_count > 0:
409 unix_first_failure = \
410 nttime2unix(failed_link.time_first_failure)
411 # TODO guard against future
412 if unix_first_failure > self.unix_now:
413 logger.error("The last success time attribute for \
414 repsFrom is in the future!")
416 # Perform calculation in seconds
417 if (self.unix_now - unix_first_failure) > 60 * 60 * 2:
418 return True
420 # TODO connections.
421 # We have checked failed *links*, but we also need to check
422 # *connections*
424 return False
426 # TODO: This should be backed by some form of local database
427 def remove_unneeded_failed_links_connections(self):
428 # Remove all tuples in kcc_failed_links where failure count = 0
429 # In this implementation, this should never happen.
431 # Remove all connections which were not used this run or connections
432 # that became active during this run.
433 pass
435 def _ensure_connections_are_loaded(self, connections):
436 """Load or fake-load NTDSConnections lacking GUIDs
438 New connections don't have GUIDs and created times which are
439 needed for sorting. If we're in read-only mode, we make fake
440 GUIDs, otherwise we ask SamDB to do it for us.
442 :param connections: an iterable of NTDSConnection objects.
443 :return: None
445 for cn_conn in connections:
446 if cn_conn.guid is None:
447 if self.readonly:
448 cn_conn.guid = misc.GUID(str(uuid.uuid4()))
449 cn_conn.whenCreated = self.nt_now
450 else:
451 cn_conn.load_connection(self.samdb)
453 def _mark_broken_ntdsconn(self):
454 """Find NTDS Connections that lack a remote
456 I'm not sure how they appear. Let's be rid of them by marking
457 them with the to_be_deleted attribute.
459 :return: None
461 for cn_conn in self.my_dsa.connect_table.values():
462 s_dnstr = cn_conn.get_from_dnstr()
463 if s_dnstr is None:
464 DEBUG_FN("%s has phantom connection %s" % (self.my_dsa,
465 cn_conn))
466 cn_conn.to_be_deleted = True
468 def _mark_unneeded_local_ntdsconn(self):
469 """Find unneeded intrasite NTDS Connections for removal
471 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections.
472 Every DC removes its own unnecessary intrasite connections.
473 This function tags them with the to_be_deleted attribute.
475 :return: None
477 # XXX should an RODC be regarded as same site? It isn't part
478 # of the intrasite ring.
480 if self.my_site.is_cleanup_ntdsconn_disabled():
481 DEBUG_FN("not doing ntdsconn cleanup for site %s, "
482 "because it is disabled" % self.my_site)
483 return
485 mydsa = self.my_dsa
487 self._ensure_connections_are_loaded(mydsa.connect_table.values())
489 local_connections = []
491 for cn_conn in mydsa.connect_table.values():
492 s_dnstr = cn_conn.get_from_dnstr()
493 if s_dnstr in self.my_site.dsa_table:
494 removable = not (cn_conn.is_generated() or
495 cn_conn.is_rodc_topology())
496 packed_guid = ndr_pack(cn_conn.guid)
497 local_connections.append((cn_conn, s_dnstr,
498 packed_guid, removable))
500 for a, b in itertools.permutations(local_connections, 2):
501 cn_conn, s_dnstr, packed_guid, removable = a
502 cn_conn2, s_dnstr2, packed_guid2, removable2 = b
503 if (removable and
504 s_dnstr == s_dnstr2 and
505 cn_conn.whenCreated < cn_conn2.whenCreated or
506 (cn_conn.whenCreated == cn_conn2.whenCreated and
507 packed_guid < packed_guid2)):
508 cn_conn.to_be_deleted = True
510 def _mark_unneeded_intersite_ntdsconn(self):
511 """find unneeded intersite NTDS Connections for removal
513 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections. The
514 intersite topology generator removes links for all DCs in its
515 site. Here we just tag them with the to_be_deleted attribute.
517 :return: None
519 # Find the intersite connections
520 local_dsas = self.my_site.dsa_table
521 connections_and_dsas = []
522 for dsa in local_dsas.values():
523 for cn in dsa.connect_table.values():
524 s_dnstr = cn.get_from_dnstr()
525 if s_dnstr not in local_dsas:
526 from_dsa = self.get_dsa(s_dnstr)
527 connections_and_dsas.append((cn, dsa, from_dsa))
529 self._ensure_connections_are_loaded(x[0] for x in connections_and_dsas)
530 for cn, to_dsa, from_dsa in connections_and_dsas:
531 if not cn.is_generated() or cn.is_rodc_topology():
532 continue
534 # If the connection is in the kept_connections list, we
535 # only remove it if an endpoint seems down.
536 if (cn in self.kept_connections and
537 not (self.is_bridgehead_failed(to_dsa, True) or
538 self.is_bridgehead_failed(from_dsa, True))):
539 continue
541 # this one is broken and might be superseded by another.
542 # But which other? Let's just say another link to the same
543 # site can supersede.
544 from_dnstr = from_dsa.dsa_dnstr
545 for site in self.site_table.values():
546 if from_dnstr in site.rw_dsa_table:
547 for cn2, to_dsa2, from_dsa2 in connections_and_dsas:
548 if (cn is not cn2 and
549 from_dsa2 in site.rw_dsa_table):
550 cn.to_be_deleted = True
552 def _commit_changes(self, dsa):
553 if dsa.is_ro() or self.readonly:
554 for connect in dsa.connect_table.values():
555 if connect.to_be_deleted:
556 logger.info("TO BE DELETED:\n%s" % connect)
557 if connect.to_be_added:
558 logger.info("TO BE ADDED:\n%s" % connect)
559 if connect.to_be_modified:
560 logger.info("TO BE MODIFIED:\n%s" % connect)
562 # Peform deletion from our tables but perform
563 # no database modification
564 dsa.commit_connections(self.samdb, ro=True)
565 else:
566 # Commit any modified connections
567 dsa.commit_connections(self.samdb)
569 def remove_unneeded_ntdsconn(self, all_connected):
570 """Remove unneeded NTDS Connections once topology is calculated
572 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections
574 :param all_connected: indicates whether all sites are connected
575 :return: None
577 self._mark_broken_ntdsconn()
578 self._mark_unneeded_local_ntdsconn()
579 # if we are not the istg, we're done!
580 # if we are the istg, but all_connected is False, we also do nothing.
581 if self.my_dsa.is_istg() and all_connected:
582 self._mark_unneeded_intersite_ntdsconn()
584 for dsa in self.my_site.dsa_table.values():
585 self._commit_changes(dsa)
588 def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
589 """Update an repsFrom object if required.
591 Part of MS-ADTS 6.2.2.5.
593 Update t_repsFrom if necessary to satisfy requirements. Such
594 updates are typically required when the IDL_DRSGetNCChanges
595 server has moved from one site to another--for example, to
596 enable compression when the server is moved from the
597 client's site to another site.
599 The repsFrom.update_flags bit field may be modified
600 auto-magically if any changes are made here. See
601 kcc_utils.RepsFromTo for gory details.
604 :param n_rep: NC replica we need
605 :param t_repsFrom: repsFrom tuple to modify
606 :param s_rep: NC replica at source DSA
607 :param s_dsa: source DSA
608 :param cn_conn: Local DSA NTDSConnection child
610 :return: None
612 s_dnstr = s_dsa.dsa_dnstr
613 same_site = s_dnstr in self.my_site.dsa_table
615 # if schedule doesn't match then update and modify
616 times = convert_schedule_to_repltimes(cn_conn.schedule)
617 if times != t_repsFrom.schedule:
618 t_repsFrom.schedule = times
620 # Bit DRS_PER_SYNC is set in replicaFlags if and only
621 # if nTDSConnection schedule has a value v that specifies
622 # scheduled replication is to be performed at least once
623 # per week.
624 if cn_conn.is_schedule_minimum_once_per_week():
626 if ((t_repsFrom.replica_flags &
627 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0):
628 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
630 # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
631 # if the source DSA and the local DC's nTDSDSA object are
632 # in the same site or source dsa is the FSMO role owner
633 # of one or more FSMO roles in the NC replica.
634 if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
636 if ((t_repsFrom.replica_flags &
637 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0):
638 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
640 # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
641 # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
642 # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
643 # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
644 # t.replicaFlags if and only if s and the local DC's
645 # nTDSDSA object are in different sites.
646 if ((cn_conn.options &
647 dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0):
649 if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
650 # WARNING
652 # it LOOKS as if this next test is a bit silly: it
653 # checks the flag then sets it if it not set; the same
654 # effect could be achieved by unconditionally setting
655 # it. But in fact the repsFrom object has special
656 # magic attached to it, and altering replica_flags has
657 # side-effects. That is bad in my opinion, but there
658 # you go.
659 if ((t_repsFrom.replica_flags &
660 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
661 t_repsFrom.replica_flags |= \
662 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
664 elif not same_site:
666 if ((t_repsFrom.replica_flags &
667 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
668 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
670 # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
671 # and only if s and the local DC's nTDSDSA object are
672 # not in the same site and the
673 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
674 # clear in cn!options
675 if (not same_site and
676 (cn_conn.options &
677 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
679 if ((t_repsFrom.replica_flags &
680 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0):
681 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
683 # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
684 # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
685 if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
687 if ((t_repsFrom.replica_flags &
688 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0):
689 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
691 # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
692 # set in t.replicaFlags if and only if cn!enabledConnection = false.
693 if not cn_conn.is_enabled():
695 if ((t_repsFrom.replica_flags &
696 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0):
697 t_repsFrom.replica_flags |= \
698 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
700 if ((t_repsFrom.replica_flags &
701 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0):
702 t_repsFrom.replica_flags |= \
703 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
705 # If s and the local DC's nTDSDSA object are in the same site,
706 # cn!transportType has no value, or the RDN of cn!transportType
707 # is CN=IP:
709 # Bit DRS_MAIL_REP in t.replicaFlags is clear.
711 # t.uuidTransport = NULL GUID.
713 # t.uuidDsa = The GUID-based DNS name of s.
715 # Otherwise:
717 # Bit DRS_MAIL_REP in t.replicaFlags is set.
719 # If x is the object with dsname cn!transportType,
720 # t.uuidTransport = x!objectGUID.
722 # Let a be the attribute identified by
723 # x!transportAddressAttribute. If a is
724 # the dNSHostName attribute, t.uuidDsa = the GUID-based
725 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
727 # It appears that the first statement i.e.
729 # "If s and the local DC's nTDSDSA object are in the same
730 # site, cn!transportType has no value, or the RDN of
731 # cn!transportType is CN=IP:"
733 # could be a slightly tighter statement if it had an "or"
734 # between each condition. I believe this should
735 # be interpreted as:
737 # IF (same-site) OR (no-value) OR (type-ip)
739 # because IP should be the primary transport mechanism
740 # (even in inter-site) and the absense of the transportType
741 # attribute should always imply IP no matter if its multi-site
743 # NOTE MS-TECH INCORRECT:
745 # All indications point to these statements above being
746 # incorrectly stated:
748 # t.uuidDsa = The GUID-based DNS name of s.
750 # Let a be the attribute identified by
751 # x!transportAddressAttribute. If a is
752 # the dNSHostName attribute, t.uuidDsa = the GUID-based
753 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
755 # because the uuidDSA is a GUID and not a GUID-base DNS
756 # name. Nor can uuidDsa hold (s!parent)!a if not
757 # dNSHostName. What should have been said is:
759 # t.naDsa = The GUID-based DNS name of s
761 # That would also be correct if transportAddressAttribute
762 # were "mailAddress" because (naDsa) can also correctly
763 # hold the SMTP ISM service address.
765 nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
767 if ((t_repsFrom.replica_flags &
768 drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0):
769 t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
771 t_repsFrom.transport_guid = misc.GUID()
773 # See (NOTE MS-TECH INCORRECT) above
775 # NOTE: it looks like these conditionals are pointless,
776 # because the state will end up as `t_repsFrom.dns_name1 ==
777 # nastr` in either case, BUT the repsFrom thing is magic and
778 # assigning to it alters some flags. So we try not to update
779 # it unless necessary.
780 if t_repsFrom.dns_name1 != nastr:
781 t_repsFrom.dns_name1 = nastr
783 if t_repsFrom.version > 0x1 and t_repsFrom.dns_name2 != nastr:
784 t_repsFrom.dns_name2 = nastr
786 if t_repsFrom.is_modified():
787 DEBUG_FN("modify_repsFrom(): %s" % t_repsFrom)
789 def get_dsa_for_implied_replica(self, n_rep, cn_conn):
790 """If a connection imply a replica, find the relevant DSA
792 Given a NC replica and NTDS Connection, determine if the
793 connection implies a repsFrom tuple should be present from the
794 source DSA listed in the connection to the naming context. If
795 it should be, return the DSA; otherwise return None.
797 Based on part of MS-ADTS 6.2.2.5
799 :param n_rep: NC replica
800 :param cn_conn: NTDS Connection
801 :return: source DSA or None
803 #XXX different conditions for "implies" than MS-ADTS 6.2.2
805 # NTDS Connection must satisfy all the following criteria
806 # to imply a repsFrom tuple is needed:
808 # cn!enabledConnection = true.
809 # cn!options does not contain NTDSCONN_OPT_RODC_TOPOLOGY.
810 # cn!fromServer references an nTDSDSA object.
812 if not cn_conn.is_enabled() or cn_conn.is_rodc_topology():
813 return None
815 s_dnstr = cn_conn.get_from_dnstr()
816 s_dsa = self.get_dsa(s_dnstr)
818 # No DSA matching this source DN string?
819 if s_dsa is None:
820 return None
822 # To imply a repsFrom tuple is needed, each of these
823 # must be True:
825 # An NC replica of the NC "is present" on the DC to
826 # which the nTDSDSA object referenced by cn!fromServer
827 # corresponds.
829 # An NC replica of the NC "should be present" on
830 # the local DC
831 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
833 if s_rep is None or not s_rep.is_present():
834 return None
836 # To imply a repsFrom tuple is needed, each of these
837 # must be True:
839 # The NC replica on the DC referenced by cn!fromServer is
840 # a writable replica or the NC replica that "should be
841 # present" on the local DC is a partial replica.
843 # The NC is not a domain NC, the NC replica that
844 # "should be present" on the local DC is a partial
845 # replica, cn!transportType has no value, or
846 # cn!transportType has an RDN of CN=IP.
848 implied = (not s_rep.is_ro() or n_rep.is_partial()) and \
849 (not n_rep.is_domain() or
850 n_rep.is_partial() or
851 cn_conn.transport_dnstr is None or
852 cn_conn.transport_dnstr.find("CN=IP") == 0)
854 if implied:
855 return s_dsa
856 return None
858 def translate_ntdsconn(self, current_dsa=None):
859 """Adjust repsFrom to match NTDSConnections
861 This function adjusts values of repsFrom abstract attributes of NC
862 replicas on the local DC to match those implied by
863 nTDSConnection objects.
865 Based on [MS-ADTS] 6.2.2.5
867 :param current_dsa: optional DSA on whose behalf we are acting.
868 :return: None
870 count = 0
872 if current_dsa is None:
873 current_dsa = self.my_dsa
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 dnstr not in needed_rep_table:
904 delete_reps.add(dnstr)
906 DEBUG_FN('current %d needed %d delete %d' % (len(current_rep_table),
907 len(needed_rep_table), len(delete_reps)))
909 if delete_reps:
910 DEBUG('deleting these reps: %s' % delete_reps)
911 for dnstr in delete_reps:
912 del current_rep_table[dnstr]
914 # Now perform the scan of replicas we'll need
915 # and compare any current repsFrom against the
916 # connections
917 for n_rep in needed_rep_table.values():
919 # load any repsFrom and fsmo roles as we'll
920 # need them during connection translation
921 n_rep.load_repsFrom(self.samdb)
922 n_rep.load_fsmo_roles(self.samdb)
924 # Loop thru the existing repsFrom tupples (if any)
925 # XXX This is a list and could contain duplicates
926 # (multiple load_repsFrom calls)
927 for t_repsFrom in n_rep.rep_repsFrom:
929 # for each tuple t in n!repsFrom, let s be the nTDSDSA
930 # object such that s!objectGUID = t.uuidDsa
931 guidstr = str(t_repsFrom.source_dsa_obj_guid)
932 s_dsa = self.get_dsa_by_guidstr(guidstr)
934 # Source dsa is gone from config (strange)
935 # so cleanup stale repsFrom for unlisted DSA
936 if s_dsa is None:
937 logger.warning("repsFrom source DSA guid (%s) not found" %
938 guidstr)
939 t_repsFrom.to_be_deleted = True
940 continue
942 # Find the connection that this repsFrom would use. If
943 # there isn't a good one (i.e. non-RODC_TOPOLOGY,
944 # meaning non-FRS), we delete the repsFrom.
945 s_dnstr = s_dsa.dsa_dnstr
946 connections = current_dsa.get_connection_by_from_dnstr(s_dnstr)
947 for cn_conn in connections:
948 if not cn_conn.is_rodc_topology():
949 break
950 else:
951 # no break means no non-rodc_topology connection exists
952 t_repsFrom.to_be_deleted = True
953 continue
955 # KCC removes this repsFrom tuple if any of the following
956 # is true:
957 # No NC replica of the NC "is present" on DSA that
958 # would be source of replica
960 # A writable replica of the NC "should be present" on
961 # the local DC, but a partial replica "is present" on
962 # the source DSA
963 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
965 if s_rep is None or not s_rep.is_present() or \
966 (not n_rep.is_ro() and s_rep.is_partial()):
968 t_repsFrom.to_be_deleted = True
969 continue
971 # If the KCC did not remove t from n!repsFrom, it updates t
972 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
974 # Loop thru connections and add implied repsFrom tuples
975 # for each NTDSConnection under our local DSA if the
976 # repsFrom is not already present
977 for cn_conn in current_dsa.connect_table.values():
979 s_dsa = self.get_dsa_for_implied_replica(n_rep, cn_conn)
980 if s_dsa is None:
981 continue
983 # Loop thru the existing repsFrom tupples (if any) and
984 # if we already have a tuple for this connection then
985 # no need to proceed to add. It will have been changed
986 # to have the correct attributes above
987 for t_repsFrom in n_rep.rep_repsFrom:
988 guidstr = str(t_repsFrom.source_dsa_obj_guid)
989 if s_dsa is self.get_dsa_by_guidstr(guidstr):
990 s_dsa = None
991 break
993 if s_dsa is None:
994 continue
996 # Create a new RepsFromTo and proceed to modify
997 # it according to specification
998 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
1000 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
1002 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
1004 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
1006 # Add to our NC repsFrom as this is newly computed
1007 if t_repsFrom.is_modified():
1008 n_rep.rep_repsFrom.append(t_repsFrom)
1010 if self.readonly:
1011 # Display any to be deleted or modified repsFrom
1012 text = n_rep.dumpstr_to_be_deleted()
1013 if text:
1014 logger.info("TO BE DELETED:\n%s" % text)
1015 text = n_rep.dumpstr_to_be_modified()
1016 if text:
1017 logger.info("TO BE MODIFIED:\n%s" % text)
1019 # Peform deletion from our tables but perform
1020 # no database modification
1021 n_rep.commit_repsFrom(self.samdb, ro=True)
1022 else:
1023 # Commit any modified repsFrom to the NC replica
1024 n_rep.commit_repsFrom(self.samdb)
1026 def merge_failed_links(self, ping=None):
1027 """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
1029 The KCC on a writable DC attempts to merge the link and connection
1030 failure information from bridgehead DCs in its own site to help it
1031 identify failed bridgehead DCs.
1033 Based on MS-ADTS 6.2.2.3.2 "Merge of kCCFailedLinks and kCCFailedLinks
1034 from Bridgeheads"
1036 :param ping: An oracle of current bridgehead availability
1037 :return: None
1039 # 1. Queries every bridgehead server in your site (other than yourself)
1040 # 2. For every ntDSConnection that references a server in a different
1041 # site merge all the failure info
1043 # XXX - not implemented yet
1044 if ping is not None:
1045 debug.DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1046 else:
1047 DEBUG_FN("skipping merge_failed_links() because it requires "
1048 "real network connections\n"
1049 "and we weren't asked to --attempt-live-connections")
1051 def setup_graph(self, part):
1052 """Set up an intersite graph
1054 An intersite graph has a Vertex for each site object, a
1055 MultiEdge for each SiteLink object, and a MutliEdgeSet for
1056 each siteLinkBridge object (or implied siteLinkBridge). It
1057 reflects the intersite topology in a slightly more abstract
1058 graph form.
1060 Roughly corresponds to MS-ADTS 6.2.2.3.4.3
1062 :param part: a Partition object
1063 :returns: an InterSiteGraph object
1065 # If 'Bridge all site links' is enabled and Win2k3 bridges required
1066 # is not set
1067 # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1068 # No documentation for this however, ntdsapi.h appears to have:
1069 # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1070 bridges_required = self.my_site.site_options & 0x00001002 != 0
1071 transport_guid = str(self.ip_transport.guid)
1073 g = setup_graph(part, self.site_table, transport_guid,
1074 self.sitelink_table, bridges_required)
1076 if self.verify or self.dot_file_dir is not None:
1077 dot_edges = []
1078 for edge in g.edges:
1079 for a, b in itertools.combinations(edge.vertices, 2):
1080 dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1081 verify_properties = ()
1082 name = 'site_edges_%s' % part.partstr
1083 verify_and_dot(name, dot_edges, directed=False,
1084 label=self.my_dsa_dnstr,
1085 properties=verify_properties, debug=DEBUG,
1086 verify=self.verify,
1087 dot_file_dir=self.dot_file_dir)
1089 return g
1091 def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1092 """Get a bridghead DC for a site.
1094 Part of MS-ADTS 6.2.2.3.4.4
1096 :param site: site object representing for which a bridgehead
1097 DC is desired.
1098 :param part: crossRef for NC to replicate.
1099 :param transport: interSiteTransport object for replication
1100 traffic.
1101 :param partial_ok: True if a DC containing a partial
1102 replica or a full replica will suffice, False if only
1103 a full replica will suffice.
1104 :param detect_failed: True to detect failed DCs and route
1105 replication traffic around them, False to assume no DC
1106 has failed.
1107 :return: dsa object for the bridgehead DC or None
1110 bhs = self.get_all_bridgeheads(site, part, transport,
1111 partial_ok, detect_failed)
1112 if len(bhs) == 0:
1113 debug.DEBUG_MAGENTA("get_bridgehead:\n\tsitedn=%s\n\tbhdn=None" %
1114 site.site_dnstr)
1115 return None
1116 else:
1117 debug.DEBUG_GREEN("get_bridgehead:\n\tsitedn=%s\n\tbhdn=%s" %
1118 (site.site_dnstr, bhs[0].dsa_dnstr))
1119 return bhs[0]
1121 def get_all_bridgeheads(self, site, part, transport,
1122 partial_ok, detect_failed):
1123 """Get all bridghead DCs on a site satisfying the given criteria
1125 Part of MS-ADTS 6.2.2.3.4.4
1127 :param site: site object representing the site for which
1128 bridgehead DCs are desired.
1129 :param part: partition for NC to replicate.
1130 :param transport: interSiteTransport object for
1131 replication traffic.
1132 :param partial_ok: True if a DC containing a partial
1133 replica or a full replica will suffice, False if
1134 only a full replica will suffice.
1135 :param detect_failed: True to detect failed DCs and route
1136 replication traffic around them, FALSE to assume
1137 no DC has failed.
1138 :return: list of dsa object for available bridgehead DCs
1140 bhs = []
1142 if transport.name != "IP":
1143 raise KCCError("get_all_bridgeheads has run into a "
1144 "non-IP transport! %r"
1145 % (transport.name,))
1147 DEBUG_FN("get_all_bridgeheads")
1148 DEBUG_FN(site.rw_dsa_table)
1149 for dsa in site.rw_dsa_table.values():
1151 pdnstr = dsa.get_parent_dnstr()
1153 # IF t!bridgeheadServerListBL has one or more values and
1154 # t!bridgeheadServerListBL does not contain a reference
1155 # to the parent object of dc then skip dc
1156 if ((len(transport.bridgehead_list) != 0 and
1157 pdnstr not in transport.bridgehead_list)):
1158 continue
1160 # IF dc is in the same site as the local DC
1161 # IF a replica of cr!nCName is not in the set of NC replicas
1162 # that "should be present" on dc or a partial replica of the
1163 # NC "should be present" but partialReplicasOkay = FALSE
1164 # Skip dc
1165 if self.my_site.same_site(dsa):
1166 needed, ro, partial = part.should_be_present(dsa)
1167 if not needed or (partial and not partial_ok):
1168 continue
1169 rep = dsa.get_current_replica(part.nc_dnstr)
1171 # ELSE
1172 # IF an NC replica of cr!nCName is not in the set of NC
1173 # replicas that "are present" on dc or a partial replica of
1174 # the NC "is present" but partialReplicasOkay = FALSE
1175 # Skip dc
1176 else:
1177 rep = dsa.get_current_replica(part.nc_dnstr)
1178 if rep is None or (rep.is_partial() and not partial_ok):
1179 continue
1181 # IF AmIRODC() and cr!nCName corresponds to default NC then
1182 # Let dsaobj be the nTDSDSA object of the dc
1183 # IF dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1184 # Skip dc
1185 if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1186 if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1187 continue
1189 # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1190 # Skip dc
1191 if self.is_bridgehead_failed(dsa, detect_failed):
1192 DEBUG("bridgehead is failed")
1193 continue
1195 DEBUG_FN("get_all_bridgeheads: dsadn=%s" % dsa.dsa_dnstr)
1196 bhs.append(dsa)
1198 # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1199 # s!options
1200 # SORT bhs such that all GC servers precede DCs that are not GC
1201 # servers, and otherwise by ascending objectGUID
1202 # ELSE
1203 # SORT bhs in a random order
1204 if site.is_random_bridgehead_disabled():
1205 bhs.sort(sort_dsa_by_gc_and_guid)
1206 else:
1207 random.shuffle(bhs)
1208 debug.DEBUG_YELLOW(bhs)
1209 return bhs
1211 def is_bridgehead_failed(self, dsa, detect_failed):
1212 """Determine whether a given DC is known to be in a failed state
1214 :param dsa: the bridgehead to test
1215 :param detect_failed: True to really check, False to assume no failure
1216 :return: True if and only if the DC should be considered failed
1218 Here we DEPART from the pseudo code spec which appears to be
1219 wrong. It says, in full:
1221 /***** BridgeheadDCFailed *****/
1222 /* Determine whether a given DC is known to be in a failed state.
1223 * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1224 * IN: detectFailedDCs - TRUE if and only failed DC detection is
1225 * enabled.
1226 * RETURNS: TRUE if and only if the DC should be considered to be in a
1227 * failed state.
1229 BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1231 IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1232 the options attribute of the site settings object for the local
1233 DC's site
1234 RETURN FALSE
1235 ELSEIF a tuple z exists in the kCCFailedLinks or
1236 kCCFailedConnections variables such that z.UUIDDsa =
1237 objectGUID, z.FailureCount > 1, and the current time -
1238 z.TimeFirstFailure > 2 hours
1239 RETURN TRUE
1240 ELSE
1241 RETURN detectFailedDCs
1242 ENDIF
1245 where you will see detectFailedDCs is not behaving as
1246 advertised -- it is acting as a default return code in the
1247 event that a failure is not detected, not a switch turning
1248 detection on or off. Elsewhere the documentation seems to
1249 concur with the comment rather than the code.
1251 if not detect_failed:
1252 return False
1254 # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1255 # When DETECT_STALE_DISABLED, we can never know of if
1256 # it's in a failed state
1257 if self.my_site.site_options & 0x00000008:
1258 return False
1260 return self.is_stale_link_connection(dsa)
1262 def create_connection(self, part, rbh, rsite, transport,
1263 lbh, lsite, link_opt, link_sched,
1264 partial_ok, detect_failed):
1265 """Create an nTDSConnection object as specified if it doesn't exist.
1267 Part of MS-ADTS 6.2.2.3.4.5
1269 :param part: crossRef object for the NC to replicate.
1270 :param rbh: nTDSDSA object for DC to act as the
1271 IDL_DRSGetNCChanges server (which is in a site other
1272 than the local DC's site).
1273 :param rsite: site of the rbh
1274 :param transport: interSiteTransport object for the transport
1275 to use for replication traffic.
1276 :param lbh: nTDSDSA object for DC to act as the
1277 IDL_DRSGetNCChanges client (which is in the local DC's site).
1278 :param lsite: site of the lbh
1279 :param link_opt: Replication parameters (aggregated siteLink options,
1280 etc.)
1281 :param link_sched: Schedule specifying the times at which
1282 to begin replicating.
1283 :partial_ok: True if bridgehead DCs containing partial
1284 replicas of the NC are acceptable.
1285 :param detect_failed: True to detect failed DCs and route
1286 replication traffic around them, FALSE to assume no DC
1287 has failed.
1289 rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1290 partial_ok, False)
1291 rbh_table = dict((x.dsa_dnstr, x) for x in rbhs_all)
1293 debug.DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all),
1294 [x.dsa_dnstr for x in rbhs_all]))
1296 # MS-TECH says to compute rbhs_avail but then doesn't use it
1297 # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1298 # partial_ok, detect_failed)
1300 lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1301 partial_ok, False)
1302 if lbh.is_ro():
1303 lbhs_all.append(lbh)
1305 debug.DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all),
1306 [x.dsa_dnstr for x in lbhs_all]))
1308 # MS-TECH says to compute lbhs_avail but then doesn't use it
1309 # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1310 # partial_ok, detect_failed)
1312 # FOR each nTDSConnection object cn such that the parent of cn is
1313 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1314 for ldsa in lbhs_all:
1315 for cn in ldsa.connect_table.values():
1317 rdsa = rbh_table.get(cn.from_dnstr)
1318 if rdsa is None:
1319 continue
1321 debug.DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1322 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1323 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1324 # cn!transportType references t
1325 if ((cn.is_generated() and
1326 not cn.is_rodc_topology() and
1327 cn.transport_guid == transport.guid)):
1329 # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1330 # cn!options and cn!schedule != sch
1331 # Perform an originating update to set cn!schedule to
1332 # sched
1333 if ((not cn.is_user_owned_schedule() and
1334 not cn.is_equivalent_schedule(link_sched))):
1335 cn.schedule = link_sched
1336 cn.set_modified(True)
1338 # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1339 # NTDSCONN_OPT_USE_NOTIFY are set in cn
1340 if cn.is_override_notify_default() and \
1341 cn.is_use_notify():
1343 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1344 # ri.Options
1345 # Perform an originating update to clear bits
1346 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1347 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1348 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1349 cn.options &= \
1350 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1351 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1352 cn.set_modified(True)
1354 # ELSE
1355 else:
1357 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1358 # ri.Options
1359 # Perform an originating update to set 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 # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1369 if cn.is_twoway_sync():
1371 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1372 # ri.Options
1373 # Perform an originating update to clear bit
1374 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1375 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1376 cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1377 cn.set_modified(True)
1379 # ELSE
1380 else:
1382 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1383 # ri.Options
1384 # Perform an originating update to set bit
1385 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1386 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1387 cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1388 cn.set_modified(True)
1390 # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1391 # in cn!options
1392 if cn.is_intersite_compression_disabled():
1394 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1395 # in ri.Options
1396 # Perform an originating update to clear bit
1397 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1398 # cn!options
1399 if ((link_opt &
1400 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0):
1401 cn.options &= \
1402 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1403 cn.set_modified(True)
1405 # ELSE
1406 else:
1407 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1408 # ri.Options
1409 # Perform an originating update to set bit
1410 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1411 # cn!options
1412 if ((link_opt &
1413 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1414 cn.options |= \
1415 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1416 cn.set_modified(True)
1418 # Display any modified connection
1419 if self.readonly:
1420 if cn.to_be_modified:
1421 logger.info("TO BE MODIFIED:\n%s" % cn)
1423 ldsa.commit_connections(self.samdb, ro=True)
1424 else:
1425 ldsa.commit_connections(self.samdb)
1426 # ENDFOR
1428 valid_connections = 0
1430 # FOR each nTDSConnection object cn such that cn!parent is
1431 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1432 for ldsa in lbhs_all:
1433 for cn in ldsa.connect_table.values():
1435 rdsa = rbh_table.get(cn.from_dnstr)
1436 if rdsa is None:
1437 continue
1439 debug.DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1441 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1442 # cn!transportType references t) and
1443 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1444 if (((not cn.is_generated() or
1445 cn.transport_guid == transport.guid) and
1446 not cn.is_rodc_topology())):
1448 # LET rguid be the objectGUID of the nTDSDSA object
1449 # referenced by cn!fromServer
1450 # LET lguid be (cn!parent)!objectGUID
1452 # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1453 # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1454 # Increment cValidConnections by 1
1455 if ((not self.is_bridgehead_failed(rdsa, detect_failed) and
1456 not self.is_bridgehead_failed(ldsa, detect_failed))):
1457 valid_connections += 1
1459 # IF keepConnections does not contain cn!objectGUID
1460 # APPEND cn!objectGUID to keepConnections
1461 self.kept_connections.add(cn)
1463 # ENDFOR
1464 debug.DEBUG_RED("valid connections %d" % valid_connections)
1465 DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1466 # IF cValidConnections = 0
1467 if valid_connections == 0:
1469 # LET opt be NTDSCONN_OPT_IS_GENERATED
1470 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1472 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1473 # SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1474 # NTDSCONN_OPT_USE_NOTIFY in opt
1475 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1476 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1477 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1479 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1480 # SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1481 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1482 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1484 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1485 # ri.Options
1486 # SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1487 if ((link_opt &
1488 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1489 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1491 # Perform an originating update to create a new nTDSConnection
1492 # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1493 # cn!options = opt, cn!transportType is a reference to t,
1494 # cn!fromServer is a reference to rbh, and cn!schedule = sch
1495 DEBUG_FN("new connection, KCC dsa: %s" % self.my_dsa.dsa_dnstr)
1496 cn = lbh.new_connection(opt, 0, transport,
1497 rbh.dsa_dnstr, link_sched)
1499 # Display any added connection
1500 if self.readonly:
1501 if cn.to_be_added:
1502 logger.info("TO BE ADDED:\n%s" % cn)
1504 lbh.commit_connections(self.samdb, ro=True)
1505 else:
1506 lbh.commit_connections(self.samdb)
1508 # APPEND cn!objectGUID to keepConnections
1509 self.kept_connections.add(cn)
1511 def add_transports(self, vertex, local_vertex, graph, detect_failed):
1512 """Build a Vertex's transport lists
1514 Each vertex has accept_red_red and accept_black lists that
1515 list what transports they accept under various conditions. The
1516 only transport that is ever accepted is IP, and a dummy extra
1517 transport called "EDGE_TYPE_ALL".
1519 Part of MS-ADTS 6.2.2.3.4.3 -- ColorVertices
1521 :param vertex: the remote vertex we are thinking about
1522 :param local_vertex: the vertex relating to the local site.
1523 :param graph: the intersite graph
1524 :param detect_failed: whether to detect failed links
1525 :return: True if some bridgeheads were not found
1527 # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1528 # here, but using vertex seems to make more sense. That is,
1529 # the docs want this:
1531 #bh = self.get_bridgehead(vertex.site, vertex.part, transport,
1532 # local_vertex.is_black(), detect_failed)
1534 # TODO WHY?????
1536 vertex.accept_red_red = []
1537 vertex.accept_black = []
1538 found_failed = False
1540 if vertex in graph.connected_vertices:
1541 t_guid = str(self.ip_transport.guid)
1543 bh = self.get_bridgehead(vertex.site, vertex.part,
1544 self.ip_transport,
1545 vertex.is_black(), detect_failed)
1546 if bh is None:
1547 if vertex.site.is_rodc_site():
1548 vertex.accept_red_red.append(t_guid)
1549 else:
1550 found_failed = True
1551 else:
1552 vertex.accept_red_red.append(t_guid)
1553 vertex.accept_black.append(t_guid)
1555 # Add additional transport to ensure another run of Dijkstra
1556 vertex.accept_red_red.append("EDGE_TYPE_ALL")
1557 vertex.accept_black.append("EDGE_TYPE_ALL")
1559 return found_failed
1561 def create_connections(self, graph, part, detect_failed):
1562 """Create intersite NTDSConnections as needed by a partition
1564 Construct an NC replica graph for the NC identified by
1565 the given crossRef, then create any additional nTDSConnection
1566 objects required.
1568 :param graph: site graph.
1569 :param part: crossRef object for NC.
1570 :param detect_failed: True to detect failed DCs and route
1571 replication traffic around them, False to assume no DC
1572 has failed.
1574 Modifies self.kept_connections by adding any connections
1575 deemed to be "in use".
1577 :return: (all_connected, found_failed_dc)
1578 (all_connected) True if the resulting NC replica graph
1579 connects all sites that need to be connected.
1580 (found_failed_dc) True if one or more failed DCs were
1581 detected.
1583 all_connected = True
1584 found_failed = False
1586 DEBUG_FN("create_connections(): enter\n"
1587 "\tpartdn=%s\n\tdetect_failed=%s" %
1588 (part.nc_dnstr, detect_failed))
1590 # XXX - This is a highly abbreviated function from the MS-TECH
1591 # ref. It creates connections between bridgeheads to all
1592 # sites that have appropriate replicas. Thus we are not
1593 # creating a minimum cost spanning tree but instead
1594 # producing a fully connected tree. This should produce
1595 # a full (albeit not optimal cost) replication topology.
1597 my_vertex = Vertex(self.my_site, part)
1598 my_vertex.color_vertex()
1600 for v in graph.vertices:
1601 v.color_vertex()
1602 if self.add_transports(v, my_vertex, graph, False):
1603 found_failed = True
1605 # No NC replicas for this NC in the site of the local DC,
1606 # so no nTDSConnection objects need be created
1607 if my_vertex.is_white():
1608 return all_connected, found_failed
1610 edge_list, n_components = get_spanning_tree_edges(graph,
1611 self.my_site,
1612 label=part.partstr)
1614 DEBUG_FN("%s Number of components: %d" %
1615 (part.nc_dnstr, n_components))
1616 if n_components > 1:
1617 all_connected = False
1619 # LET partialReplicaOkay be TRUE if and only if
1620 # localSiteVertex.Color = COLOR.BLACK
1621 partial_ok = my_vertex.is_black()
1623 # Utilize the IP transport only for now
1624 transport = self.ip_transport
1626 DEBUG("edge_list %s" % edge_list)
1627 for e in edge_list:
1628 # XXX more accurate comparison?
1629 if e.directed and e.vertices[0].site is self.my_site:
1630 continue
1632 if e.vertices[0].site is self.my_site:
1633 rsite = e.vertices[1].site
1634 else:
1635 rsite = e.vertices[0].site
1637 # We don't make connections to our own site as that
1638 # is intrasite topology generator's job
1639 if rsite is self.my_site:
1640 DEBUG("rsite is my_site")
1641 continue
1643 # Determine bridgehead server in remote site
1644 rbh = self.get_bridgehead(rsite, part, transport,
1645 partial_ok, detect_failed)
1646 if rbh is None:
1647 continue
1649 # RODC acts as an BH for itself
1650 # IF AmIRODC() then
1651 # LET lbh be the nTDSDSA object of the local DC
1652 # ELSE
1653 # LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1654 # cr, t, partialReplicaOkay, detectFailedDCs)
1655 if self.my_dsa.is_ro():
1656 lsite = self.my_site
1657 lbh = self.my_dsa
1658 else:
1659 lsite = self.my_site
1660 lbh = self.get_bridgehead(lsite, part, transport,
1661 partial_ok, detect_failed)
1662 # TODO
1663 if lbh is None:
1664 debug.DEBUG_RED("DISASTER! lbh is None")
1665 return False, True
1667 DEBUG_FN("lsite: %s\nrsite: %s" % (lsite, rsite))
1668 DEBUG_FN("vertices %s" % (e.vertices,))
1669 debug.DEBUG_BLUE("bridgeheads\n%s\n%s\n%s" % (lbh, rbh, "-" * 70))
1671 sitelink = e.site_link
1672 if sitelink is None:
1673 link_opt = 0x0
1674 link_sched = None
1675 else:
1676 link_opt = sitelink.options
1677 link_sched = sitelink.schedule
1679 self.create_connection(part, rbh, rsite, transport,
1680 lbh, lsite, link_opt, link_sched,
1681 partial_ok, detect_failed)
1683 return all_connected, found_failed
1685 def create_intersite_connections(self):
1686 """Create NTDSConnections as necessary for all partitions.
1688 Computes an NC replica graph for each NC replica that "should be
1689 present" on the local DC or "is present" on any DC in the same site
1690 as the local DC. For each edge directed to an NC replica on such a
1691 DC from an NC replica on a DC in another site, the KCC creates an
1692 nTDSConnection object to imply that edge if one does not already
1693 exist.
1695 Modifies self.kept_connections - A set of nTDSConnection
1696 objects for edges that are directed
1697 to the local DC's site in one or more NC replica graphs.
1699 :return: True if spanning trees were created for all NC replica
1700 graphs, otherwise False.
1702 all_connected = True
1703 self.kept_connections = set()
1705 # LET crossRefList be the set containing each object o of class
1706 # crossRef such that o is a child of the CN=Partitions child of the
1707 # config NC
1709 # FOR each crossRef object cr in crossRefList
1710 # IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1711 # is clear in cr!systemFlags, skip cr.
1712 # LET g be the GRAPH return of SetupGraph()
1714 for part in self.part_table.values():
1716 if not part.is_enabled():
1717 continue
1719 if part.is_foreign():
1720 continue
1722 graph = self.setup_graph(part)
1724 # Create nTDSConnection objects, routing replication traffic
1725 # around "failed" DCs.
1726 found_failed = False
1728 connected, found_failed = self.create_connections(graph,
1729 part, True)
1731 DEBUG("with detect_failed: connected %s Found failed %s" %
1732 (connected, found_failed))
1733 if not connected:
1734 all_connected = False
1736 if found_failed:
1737 # One or more failed DCs preclude use of the ideal NC
1738 # replica graph. Add connections for the ideal graph.
1739 self.create_connections(graph, part, False)
1741 return all_connected
1743 def intersite(self, ping):
1744 """Generate the inter-site KCC replica graph and nTDSConnections
1746 As per MS-ADTS 6.2.2.3.
1748 If self.readonly is False, the connections are added to self.samdb.
1750 Produces self.kept_connections which is a set of NTDS
1751 Connections that should be kept during subsequent pruning
1752 process.
1754 After this has run, all sites should be connected in a minimum
1755 spanning tree.
1757 :param ping: An oracle function of remote site availability
1758 :return (True or False): (True) if the produced NC replica
1759 graph connects all sites that need to be connected
1762 # Retrieve my DSA
1763 mydsa = self.my_dsa
1764 mysite = self.my_site
1765 all_connected = True
1767 DEBUG_FN("intersite(): enter")
1769 # Determine who is the ISTG
1770 if self.readonly:
1771 mysite.select_istg(self.samdb, mydsa, ro=True)
1772 else:
1773 mysite.select_istg(self.samdb, mydsa, ro=False)
1775 # Test whether local site has topology disabled
1776 if mysite.is_intersite_topology_disabled():
1777 DEBUG_FN("intersite(): exit disabled all_connected=%d" %
1778 all_connected)
1779 return all_connected
1781 if not mydsa.is_istg():
1782 DEBUG_FN("intersite(): exit not istg all_connected=%d" %
1783 all_connected)
1784 return all_connected
1786 self.merge_failed_links(ping)
1788 # For each NC with an NC replica that "should be present" on the
1789 # local DC or "is present" on any DC in the same site as the
1790 # local DC, the KCC constructs a site graph--a precursor to an NC
1791 # replica graph. The site connectivity for a site graph is defined
1792 # by objects of class interSiteTransport, siteLink, and
1793 # siteLinkBridge in the config NC.
1795 all_connected = self.create_intersite_connections()
1797 DEBUG_FN("intersite(): exit all_connected=%d" % all_connected)
1798 return all_connected
1800 def update_rodc_connection(self):
1801 """Updates the RODC NTFRS connection object.
1803 If the local DSA is not an RODC, this does nothing.
1805 if not self.my_dsa.is_ro():
1806 return
1808 # Given an nTDSConnection object cn1, such that cn1.options contains
1809 # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1810 # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1811 # that the following is true:
1813 # cn1.fromServer = cn2.fromServer
1814 # cn1.schedule = cn2.schedule
1816 # If no such cn2 can be found, cn1 is not modified.
1817 # If no such cn1 can be found, nothing is modified by this task.
1819 all_connections = self.my_dsa.connect_table.values()
1820 ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1821 rw_connections = [x for x in all_connections
1822 if x not in ro_connections]
1824 # XXX here we are dealing with multiple RODC_TOPO connections,
1825 # if they exist. It is not clear whether the spec means that
1826 # or if it ever arises.
1827 if rw_connections and ro_connections:
1828 for con in ro_connections:
1829 cn2 = rw_connections[0]
1830 con.from_dnstr = cn2.from_dnstr
1831 con.schedule = cn2.schedule
1832 con.to_be_modified = True
1834 self.my_dsa.commit_connections(self.samdb, ro=self.readonly)
1836 def intrasite_max_node_edges(self, node_count):
1837 """Find the maximum number of edges directed to an intrasite node
1839 The KCC does not create more than 50 edges directed to a
1840 single DC. To optimize replication, we compute that each node
1841 should have n+2 total edges directed to it such that (n) is
1842 the smallest non-negative integer satisfying
1843 (node_count <= 2*(n*n) + 6*n + 7)
1845 (If the number of edges is m (i.e. n + 2), that is the same as
1846 2 * m*m - 2 * m + 3). We think in terms of n because that is
1847 the number of extra connections over the double directed ring
1848 that exists by default.
1850 edges n nodecount
1851 2 0 7
1852 3 1 15
1853 4 2 27
1854 5 3 43
1856 50 48 4903
1858 :param node_count: total number of nodes in the replica graph
1860 The intention is that there should be no more than 3 hops
1861 between any two DSAs at a site. With up to 7 nodes the 2 edges
1862 of the ring are enough; any configuration of extra edges with
1863 8 nodes will be enough. It is less clear that the 3 hop
1864 guarantee holds at e.g. 15 nodes in degenerate cases, but
1865 those are quite unlikely given the extra edges are randomly
1866 arranged.
1868 :param node_count: the number of nodes in the site
1869 "return: The desired maximum number of connections
1871 n = 0
1872 while True:
1873 if node_count <= (2 * (n * n) + (6 * n) + 7):
1874 break
1875 n = n + 1
1876 n = n + 2
1877 if n < 50:
1878 return n
1879 return 50
1881 def construct_intrasite_graph(self, site_local, dc_local,
1882 nc_x, gc_only, detect_stale):
1883 """Create an intrasite graph using given parameters
1885 This might be called a number of times per site with different
1886 parameters.
1888 Based on [MS-ADTS] 6.2.2.2
1890 :param site_local: site for which we are working
1891 :param dc_local: local DC that potentially needs a replica
1892 :param nc_x: naming context (x) that we are testing if it
1893 "should be present" on the local DC
1894 :param gc_only: Boolean - only consider global catalog servers
1895 :param detect_stale: Boolean - check whether links seems down
1896 :return: None
1898 # We're using the MS notation names here to allow
1899 # correlation back to the published algorithm.
1901 # nc_x - naming context (x) that we are testing if it
1902 # "should be present" on the local DC
1903 # f_of_x - replica (f) found on a DC (s) for NC (x)
1904 # dc_s - DC where f_of_x replica was found
1905 # dc_local - local DC that potentially needs a replica
1906 # (f_of_x)
1907 # r_list - replica list R
1908 # p_of_x - replica (p) is partial and found on a DC (s)
1909 # for NC (x)
1910 # l_of_x - replica (l) is the local replica for NC (x)
1911 # that should appear on the local DC
1912 # r_len = is length of replica list |R|
1914 # If the DSA doesn't need a replica for this
1915 # partition (NC x) then continue
1916 needed, ro, partial = nc_x.should_be_present(dc_local)
1918 debug.DEBUG_YELLOW("construct_intrasite_graph(): enter" +
1919 "\n\tgc_only=%d" % gc_only +
1920 "\n\tdetect_stale=%d" % detect_stale +
1921 "\n\tneeded=%s" % needed +
1922 "\n\tro=%s" % ro +
1923 "\n\tpartial=%s" % partial +
1924 "\n%s" % nc_x)
1926 if not needed:
1927 debug.DEBUG_RED("%s lacks 'should be present' status, "
1928 "aborting construct_intersite_graph!" %
1929 nc_x.nc_dnstr)
1930 return
1932 # Create a NCReplica that matches what the local replica
1933 # should say. We'll use this below in our r_list
1934 l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid,
1935 nc_x.nc_dnstr)
1937 l_of_x.identify_by_basedn(self.samdb)
1939 l_of_x.rep_partial = partial
1940 l_of_x.rep_ro = ro
1942 # Add this replica that "should be present" to the
1943 # needed replica table for this DSA
1944 dc_local.add_needed_replica(l_of_x)
1946 # Replica list
1948 # Let R be a sequence containing each writable replica f of x
1949 # such that f "is present" on a DC s satisfying the following
1950 # criteria:
1952 # * s is a writable DC other than the local DC.
1954 # * s is in the same site as the local DC.
1956 # * If x is a read-only full replica and x is a domain NC,
1957 # then the DC's functional level is at least
1958 # DS_BEHAVIOR_WIN2008.
1960 # * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
1961 # in the options attribute of the site settings object for
1962 # the local DC's site, or no tuple z exists in the
1963 # kCCFailedLinks or kCCFailedConnections variables such
1964 # that z.UUIDDsa is the objectGUID of the nTDSDSA object
1965 # for s, z.FailureCount > 0, and the current time -
1966 # z.TimeFirstFailure > 2 hours.
1968 r_list = []
1970 # We'll loop thru all the DSAs looking for
1971 # writeable NC replicas that match the naming
1972 # context dn for (nc_x)
1974 for dc_s in self.my_site.dsa_table.values():
1975 # If this partition (nc_x) doesn't appear as a
1976 # replica (f_of_x) on (dc_s) then continue
1977 if not nc_x.nc_dnstr in dc_s.current_rep_table:
1978 continue
1980 # Pull out the NCReplica (f) of (x) with the dn
1981 # that matches NC (x) we are examining.
1982 f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
1984 # Replica (f) of NC (x) must be writable
1985 if f_of_x.is_ro():
1986 continue
1988 # Replica (f) of NC (x) must satisfy the
1989 # "is present" criteria for DC (s) that
1990 # it was found on
1991 if not f_of_x.is_present():
1992 continue
1994 # DC (s) must be a writable DSA other than
1995 # my local DC. In other words we'd only replicate
1996 # from other writable DC
1997 if dc_s.is_ro() or dc_s is dc_local:
1998 continue
2000 # Certain replica graphs are produced only
2001 # for global catalogs, so test against
2002 # method input parameter
2003 if gc_only and not dc_s.is_gc():
2004 continue
2006 # DC (s) must be in the same site as the local DC
2007 # as this is the intra-site algorithm. This is
2008 # handled by virtue of placing DSAs in per
2009 # site objects (see enclosing for() loop)
2011 # If NC (x) is intended to be read-only full replica
2012 # for a domain NC on the target DC then the source
2013 # DC should have functional level at minimum WIN2008
2015 # Effectively we're saying that in order to replicate
2016 # to a targeted RODC (which was introduced in Windows 2008)
2017 # then we have to replicate from a DC that is also minimally
2018 # at that level.
2020 # You can also see this requirement in the MS special
2021 # considerations for RODC which state that to deploy
2022 # an RODC, at least one writable domain controller in
2023 # the domain must be running Windows Server 2008
2024 if ro and not partial and nc_x.nc_type == NCType.domain:
2025 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2026 continue
2028 # If we haven't been told to turn off stale connection
2029 # detection and this dsa has a stale connection then
2030 # continue
2031 if detect_stale and self.is_stale_link_connection(dc_s):
2032 continue
2034 # Replica meets criteria. Add it to table indexed
2035 # by the GUID of the DC that it appears on
2036 r_list.append(f_of_x)
2038 # If a partial (not full) replica of NC (x) "should be present"
2039 # on the local DC, append to R each partial replica (p of x)
2040 # such that p "is present" on a DC satisfying the same
2041 # criteria defined above for full replica DCs.
2043 # XXX This loop and the previous one differ only in whether
2044 # the replica is partial or not. here we only accept partial
2045 # (because we're partial); before we only accepted full. Order
2046 # doen't matter (the list is sorted a few lines down) so these
2047 # loops could easily be merged. Or this could be a helper
2048 # function.
2050 if partial:
2051 # Now we loop thru all the DSAs looking for
2052 # partial NC replicas that match the naming
2053 # context dn for (NC x)
2054 for dc_s in self.my_site.dsa_table.values():
2056 # If this partition NC (x) doesn't appear as a
2057 # replica (p) of NC (x) on the dsa DC (s) then
2058 # continue
2059 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2060 continue
2062 # Pull out the NCReplica with the dn that
2063 # matches NC (x) we are examining.
2064 p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2066 # Replica (p) of NC (x) must be partial
2067 if not p_of_x.is_partial():
2068 continue
2070 # Replica (p) of NC (x) must satisfy the
2071 # "is present" criteria for DC (s) that
2072 # it was found on
2073 if not p_of_x.is_present():
2074 continue
2076 # DC (s) must be a writable DSA other than
2077 # my DSA. In other words we'd only replicate
2078 # from other writable DSA
2079 if dc_s.is_ro() or dc_s is dc_local:
2080 continue
2082 # Certain replica graphs are produced only
2083 # for global catalogs, so test against
2084 # method input parameter
2085 if gc_only and not dc_s.is_gc():
2086 continue
2088 # If we haven't been told to turn off stale connection
2089 # detection and this dsa has a stale connection then
2090 # continue
2091 if detect_stale and self.is_stale_link_connection(dc_s):
2092 continue
2094 # Replica meets criteria. Add it to table indexed
2095 # by the GUID of the DSA that it appears on
2096 r_list.append(p_of_x)
2098 # Append to R the NC replica that "should be present"
2099 # on the local DC
2100 r_list.append(l_of_x)
2102 r_list.sort(sort_replica_by_dsa_guid)
2103 r_len = len(r_list)
2105 max_node_edges = self.intrasite_max_node_edges(r_len)
2107 # Add a node for each r_list element to the replica graph
2108 graph_list = []
2109 for rep in r_list:
2110 node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2111 graph_list.append(node)
2113 # For each r(i) from (0 <= i < |R|-1)
2114 i = 0
2115 while i < (r_len-1):
2116 # Add an edge from r(i) to r(i+1) if r(i) is a full
2117 # replica or r(i+1) is a partial replica
2118 if not r_list[i].is_partial() or r_list[i+1].is_partial():
2119 graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
2121 # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2122 # replica or ri is a partial replica.
2123 if not r_list[i+1].is_partial() or r_list[i].is_partial():
2124 graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
2125 i = i + 1
2127 # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2128 # or r0 is a partial replica.
2129 if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
2130 graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
2132 # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2133 # r|R|-1 is a partial replica.
2134 if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
2135 graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
2137 DEBUG("r_list is length %s" % len(r_list))
2138 DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr))
2139 for x in r_list))
2141 do_dot_files = self.dot_file_dir is not None and self.debug
2142 if self.verify or do_dot_files:
2143 dot_edges = []
2144 dot_vertices = set()
2145 for v1 in graph_list:
2146 dot_vertices.add(v1.dsa_dnstr)
2147 for v2 in v1.edge_from:
2148 dot_edges.append((v2, v1.dsa_dnstr))
2149 dot_vertices.add(v2)
2151 verify_properties = ('connected',)
2152 verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2153 label='%s__%s__%s' % (site_local.site_dnstr,
2154 nctype_lut[nc_x.nc_type],
2155 nc_x.nc_dnstr),
2156 properties=verify_properties, debug=DEBUG,
2157 verify=self.verify,
2158 dot_file_dir=self.dot_file_dir,
2159 directed=True)
2161 rw_dot_vertices = set(x for x in dot_vertices
2162 if not self.get_dsa(x).is_ro())
2163 rw_dot_edges = [(a, b) for a, b in dot_edges if
2164 a in rw_dot_vertices and b in rw_dot_vertices]
2165 print rw_dot_edges, rw_dot_vertices
2166 rw_verify_properties = ('connected',
2167 'directed_double_ring_or_small')
2168 verify_and_dot('intrasite_rw_pre_ntdscon', rw_dot_edges,
2169 rw_dot_vertices,
2170 label='%s__%s__%s' % (site_local.site_dnstr,
2171 nctype_lut[nc_x.nc_type],
2172 nc_x.nc_dnstr),
2173 properties=rw_verify_properties, debug=DEBUG,
2174 verify=self.verify,
2175 dot_file_dir=self.dot_file_dir,
2176 directed=True)
2178 # For each existing nTDSConnection object implying an edge
2179 # from rj of R to ri such that j != i, an edge from rj to ri
2180 # is not already in the graph, and the total edges directed
2181 # to ri is less than n+2, the KCC adds that edge to the graph.
2182 for vertex in graph_list:
2183 dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2184 for connect in dsa.connect_table.values():
2185 remote = connect.from_dnstr
2186 if remote in self.my_site.dsa_table:
2187 vertex.add_edge_from(remote)
2189 DEBUG('reps are: %s' % ' '.join(x.rep_dsa_dnstr for x in r_list))
2190 DEBUG('dsas are: %s' % ' '.join(x.dsa_dnstr for x in graph_list))
2192 for tnode in graph_list:
2193 # To optimize replication latency in sites with many NC
2194 # replicas, the KCC adds new edges directed to ri to bring
2195 # the total edges to n+2, where the NC replica rk of R
2196 # from which the edge is directed is chosen at random such
2197 # that k != i and an edge from rk to ri is not already in
2198 # the graph.
2200 # Note that the KCC tech ref does not give a number for
2201 # the definition of "sites with many NC replicas". At a
2202 # bare minimum to satisfy n+2 edges directed at a node we
2203 # have to have at least three replicas in |R| (i.e. if n
2204 # is zero then at least replicas from two other graph
2205 # nodes may direct edges to us).
2206 if r_len >= 3 and not tnode.has_sufficient_edges():
2207 candidates = [x for x in graph_list if
2208 (x is not tnode and
2209 x.dsa_dnstr not in tnode.edge_from)]
2211 debug.DEBUG_BLUE("looking for random link for %s. r_len %d, "
2212 "graph len %d candidates %d"
2213 % (tnode.dsa_dnstr, r_len, len(graph_list),
2214 len(candidates)))
2216 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2218 while candidates and not tnode.has_sufficient_edges():
2219 other = random.choice(candidates)
2220 DEBUG("trying to add candidate %s" % other.dsa_dstr)
2221 if not tnode.add_edge_from(other):
2222 debug.DEBUG_RED("could not add %s" % other.dsa_dstr)
2223 candidates.remove(other)
2224 else:
2225 DEBUG_FN("not adding links to %s: nodes %s, links is %s/%s" %
2226 (tnode.dsa_dnstr, r_len, len(tnode.edge_from),
2227 tnode.max_edges))
2229 # Print the graph node in debug mode
2230 DEBUG_FN("%s" % tnode)
2232 # For each edge directed to the local DC, ensure a nTDSConnection
2233 # points to us that satisfies the KCC criteria
2235 if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2236 tnode.add_connections_from_edges(dc_local)
2238 if self.verify or do_dot_files:
2239 dot_edges = []
2240 dot_vertices = set()
2241 for v1 in graph_list:
2242 dot_vertices.add(v1.dsa_dnstr)
2243 for v2 in v1.edge_from:
2244 dot_edges.append((v2, v1.dsa_dnstr))
2245 dot_vertices.add(v2)
2247 verify_properties = ('connected',)
2248 verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2249 label='%s__%s__%s' % (site_local.site_dnstr,
2250 nctype_lut[nc_x.nc_type],
2251 nc_x.nc_dnstr),
2252 properties=verify_properties, debug=DEBUG,
2253 verify=self.verify,
2254 dot_file_dir=self.dot_file_dir,
2255 directed=True)
2257 rw_dot_vertices = set(x for x in dot_vertices
2258 if not self.get_dsa(x).is_ro())
2259 rw_dot_edges = [(a, b) for a, b in dot_edges if
2260 a in rw_dot_vertices and b in rw_dot_vertices]
2261 print rw_dot_edges, rw_dot_vertices
2262 rw_verify_properties = ('connected',
2263 'directed_double_ring_or_small')
2264 verify_and_dot('intrasite_rw_post_ntdscon', rw_dot_edges,
2265 rw_dot_vertices,
2266 label='%s__%s__%s' % (site_local.site_dnstr,
2267 nctype_lut[nc_x.nc_type],
2268 nc_x.nc_dnstr),
2269 properties=rw_verify_properties, debug=DEBUG,
2270 verify=self.verify,
2271 dot_file_dir=self.dot_file_dir,
2272 directed=True)
2274 def intrasite(self):
2275 """Generate the intrasite KCC connections
2277 As per MS-ADTS 6.2.2.2.
2279 If self.readonly is False, the connections are added to self.samdb.
2281 After this call, all DCs in each site with more than 3 DCs
2282 should be connected in a bidirectional ring. If a site has 2
2283 DCs, they will bidirectionally connected. Sites with many DCs
2284 may have arbitrary extra connections.
2286 :return: None
2288 mydsa = self.my_dsa
2290 DEBUG_FN("intrasite(): enter")
2292 # Test whether local site has topology disabled
2293 mysite = self.my_site
2294 if mysite.is_intrasite_topology_disabled():
2295 return
2297 detect_stale = (not mysite.is_detect_stale_disabled())
2298 for connect in mydsa.connect_table.values():
2299 if connect.to_be_added:
2300 debug.DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2302 # Loop thru all the partitions, with gc_only False
2303 for partdn, part in self.part_table.items():
2304 self.construct_intrasite_graph(mysite, mydsa, part, False,
2305 detect_stale)
2306 for connect in mydsa.connect_table.values():
2307 if connect.to_be_added:
2308 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2310 # If the DC is a GC server, the KCC constructs an additional NC
2311 # replica graph (and creates nTDSConnection objects) for the
2312 # config NC as above, except that only NC replicas that "are present"
2313 # on GC servers are added to R.
2314 for connect in mydsa.connect_table.values():
2315 if connect.to_be_added:
2316 debug.DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2318 # Do it again, with gc_only True
2319 for partdn, part in self.part_table.items():
2320 if part.is_config():
2321 self.construct_intrasite_graph(mysite, mydsa, part, True,
2322 detect_stale)
2324 # The DC repeats the NC replica graph computation and nTDSConnection
2325 # creation for each of the NC replica graphs, this time assuming
2326 # that no DC has failed. It does so by re-executing the steps as
2327 # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2328 # set in the options attribute of the site settings object for
2329 # the local DC's site. (ie. we set "detec_stale" flag to False)
2330 for connect in mydsa.connect_table.values():
2331 if connect.to_be_added:
2332 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2334 # Loop thru all the partitions.
2335 for partdn, part in self.part_table.items():
2336 self.construct_intrasite_graph(mysite, mydsa, part, False,
2337 False) # don't detect stale
2339 # If the DC is a GC server, the KCC constructs an additional NC
2340 # replica graph (and creates nTDSConnection objects) for the
2341 # config NC as above, except that only NC replicas that "are present"
2342 # on GC servers are added to R.
2343 for connect in mydsa.connect_table.values():
2344 if connect.to_be_added:
2345 debug.DEBUG_RED("TO BE ADDED:\n%s" % connect)
2347 for partdn, part in self.part_table.items():
2348 if part.is_config():
2349 self.construct_intrasite_graph(mysite, mydsa, part, True,
2350 False) # don't detect stale
2352 self._commit_changes(mydsa)
2354 def list_dsas(self):
2355 """Compile a comprehensive list of DSA DNs
2357 These are all the DSAs on all the sites that KCC would be
2358 dealing with.
2360 This method is not idempotent and may not work correctly in
2361 sequence with KCC.run().
2363 :return: a list of DSA DN strings.
2365 self.load_my_site()
2366 self.load_my_dsa()
2368 self.load_all_sites()
2369 self.load_all_partitions()
2370 self.load_ip_transport()
2371 self.load_all_sitelinks()
2372 dsas = []
2373 for site in self.site_table.values():
2374 dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2375 for dsa in site.dsa_table.values()])
2376 return dsas
2378 def load_samdb(self, dburl, lp, creds):
2379 """Load the database using an url, loadparm, and credentials
2381 :param dburl: a database url.
2382 :param lp: a loadparm object.
2383 :param creds: a Credentials object.
2385 self.samdb = SamDB(url=dburl,
2386 session_info=system_session(),
2387 credentials=creds, lp=lp)
2389 def plot_all_connections(self, basename, verify_properties=()):
2390 """Helper function to plot and verify NTDSConnections
2392 :param basename: an identifying string to use in filenames and logs.
2393 :param verify_properties: properties to verify (default empty)
2395 verify = verify_properties and self.verify
2396 if not verify and self.dot_file_dir is None:
2397 return
2399 dot_edges = []
2400 dot_vertices = []
2401 edge_colours = []
2402 vertex_colours = []
2404 for dsa in self.dsa_by_dnstr.values():
2405 dot_vertices.append(dsa.dsa_dnstr)
2406 if dsa.is_ro():
2407 vertex_colours.append('#cc0000')
2408 else:
2409 vertex_colours.append('#0000cc')
2410 for con in dsa.connect_table.values():
2411 if con.is_rodc_topology():
2412 edge_colours.append('red')
2413 else:
2414 edge_colours.append('blue')
2415 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2417 verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2418 label=self.my_dsa_dnstr,
2419 properties=verify_properties, debug=DEBUG,
2420 verify=verify, dot_file_dir=self.dot_file_dir,
2421 directed=True, edge_colors=edge_colours,
2422 vertex_colors=vertex_colours)
2424 def run(self, dburl, lp, creds, forced_local_dsa=None,
2425 forget_local_links=False, forget_intersite_links=False,
2426 attempt_live_connections=False):
2427 """Perform a KCC run, possibly updating repsFrom topology
2429 :param dburl: url of the database to work with.
2430 :param lp: a loadparm object.
2431 :param creds: a Credentials object.
2432 :param forced_local_dsa: pretend to be on the DSA with this dn_str
2433 :param forget_local_links: calculate as if no connections existed
2434 (boolean, default False)
2435 :param forget_intersite_links: calculate with only intrasite connection
2436 (boolean, default False)
2437 :param attempt_live_connections: attempt to connect to remote DSAs to
2438 determine link availability (boolean, default False)
2439 :return: 1 on error, 0 otherwise
2441 # We may already have a samdb setup if we are
2442 # currently importing an ldif for a test run
2443 if self.samdb is None:
2444 try:
2445 self.load_samdb(dburl, lp, creds)
2446 except ldb.LdbError, (num, msg):
2447 logger.error("Unable to open sam database %s : %s" %
2448 (dburl, msg))
2449 return 1
2451 if forced_local_dsa:
2452 self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" %
2453 forced_local_dsa)
2455 try:
2456 # Setup
2457 self.load_my_site()
2458 self.load_my_dsa()
2460 self.load_all_sites()
2461 self.load_all_partitions()
2462 self.load_ip_transport()
2463 self.load_all_sitelinks()
2465 if self.verify or self.dot_file_dir is not None:
2466 guid_to_dnstr = {}
2467 for site in self.site_table.values():
2468 guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2469 for dnstr, dsa
2470 in site.dsa_table.items())
2472 self.plot_all_connections('dsa_initial')
2474 dot_edges = []
2475 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2476 for dnstr, c_rep in current_reps.items():
2477 DEBUG("c_rep %s" % c_rep)
2478 dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2480 verify_and_dot('dsa_repsFrom_initial', dot_edges,
2481 directed=True, label=self.my_dsa_dnstr,
2482 properties=(), debug=DEBUG, verify=self.verify,
2483 dot_file_dir=self.dot_file_dir)
2485 dot_edges = []
2486 for site in self.site_table.values():
2487 for dsa in site.dsa_table.values():
2488 current_reps, needed_reps = dsa.get_rep_tables()
2489 for dn_str, rep in current_reps.items():
2490 for reps_from in rep.rep_repsFrom:
2491 DEBUG("rep %s" % rep)
2492 dsa_guid = str(reps_from.source_dsa_obj_guid)
2493 dsa_dn = guid_to_dnstr[dsa_guid]
2494 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2496 verify_and_dot('dsa_repsFrom_initial_all', dot_edges,
2497 directed=True, label=self.my_dsa_dnstr,
2498 properties=(), debug=DEBUG, verify=self.verify,
2499 dot_file_dir=self.dot_file_dir)
2501 dot_edges = []
2502 for link in self.sitelink_table.values():
2503 for a, b in itertools.combinations(link.site_list, 2):
2504 dot_edges.append((str(a), str(b)))
2505 properties = ('connected',)
2506 verify_and_dot('dsa_sitelink_initial', dot_edges,
2507 directed=False,
2508 label=self.my_dsa_dnstr, properties=properties,
2509 debug=DEBUG, verify=self.verify,
2510 dot_file_dir=self.dot_file_dir)
2512 if forget_local_links:
2513 for dsa in self.my_site.dsa_table.values():
2514 dsa.connect_table = dict((k, v) for k, v in
2515 dsa.connect_table.items()
2516 if v.is_rodc_topology())
2517 self.plot_all_connections('dsa_forgotten_local')
2519 if forget_intersite_links:
2520 for site in self.site_table.values():
2521 for dsa in site.dsa_table.values():
2522 dsa.connect_table = dict((k, v) for k, v in
2523 dsa.connect_table.items()
2524 if site is self.my_site and
2525 v.is_rodc_topology())
2527 self.plot_all_connections('dsa_forgotten_all')
2529 if attempt_live_connections:
2530 # Encapsulates lp and creds in a function that
2531 # attempts connections to remote DSAs.
2532 def ping(self, dnsname):
2533 try:
2534 drs_utils.drsuapi_connect(dnsname, self.lp, self.creds)
2535 except drs_utils.drsException:
2536 return False
2537 return True
2538 else:
2539 ping = None
2540 # These are the published steps (in order) for the
2541 # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2543 # Step 1
2544 self.refresh_failed_links_connections(ping)
2546 # Step 2
2547 self.intrasite()
2549 # Step 3
2550 all_connected = self.intersite(ping)
2552 # Step 4
2553 self.remove_unneeded_ntdsconn(all_connected)
2555 # Step 5
2556 self.translate_ntdsconn()
2558 # Step 6
2559 self.remove_unneeded_failed_links_connections()
2561 # Step 7
2562 self.update_rodc_connection()
2564 if self.verify or self.dot_file_dir is not None:
2565 self.plot_all_connections('dsa_final',
2566 ('connected',))
2568 debug.DEBUG_MAGENTA("there are %d dsa guids" %
2569 len(guid_to_dnstr))
2571 dot_edges = []
2572 edge_colors = []
2573 my_dnstr = self.my_dsa.dsa_dnstr
2574 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2575 for dnstr, n_rep in needed_reps.items():
2576 for reps_from in n_rep.rep_repsFrom:
2577 guid_str = str(reps_from.source_dsa_obj_guid)
2578 dot_edges.append((my_dnstr, guid_to_dnstr[guid_str]))
2579 edge_colors.append('#' + str(n_rep.nc_guid)[:6])
2581 verify_and_dot('dsa_repsFrom_final', dot_edges, directed=True,
2582 label=self.my_dsa_dnstr,
2583 properties=(), debug=DEBUG, verify=self.verify,
2584 dot_file_dir=self.dot_file_dir,
2585 edge_colors=edge_colors)
2587 dot_edges = []
2589 for site in self.site_table.values():
2590 for dsa in site.dsa_table.values():
2591 current_reps, needed_reps = dsa.get_rep_tables()
2592 for n_rep in needed_reps.values():
2593 for reps_from in n_rep.rep_repsFrom:
2594 dsa_guid = str(reps_from.source_dsa_obj_guid)
2595 dsa_dn = guid_to_dnstr[dsa_guid]
2596 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2598 verify_and_dot('dsa_repsFrom_final_all', dot_edges,
2599 directed=True, label=self.my_dsa_dnstr,
2600 properties=(), debug=DEBUG, verify=self.verify,
2601 dot_file_dir=self.dot_file_dir)
2603 except:
2604 raise
2606 return 0
2608 def import_ldif(self, dburl, lp, creds, ldif_file, forced_local_dsa=None):
2609 """Import relevant objects and attributes from an LDIF file.
2611 The point of this function is to allow a programmer/debugger to
2612 import an LDIF file with non-security relevent information that
2613 was previously extracted from a DC database. The LDIF file is used
2614 to create a temporary abbreviated database. The KCC algorithm can
2615 then run against this abbreviated database for debug or test
2616 verification that the topology generated is computationally the
2617 same between different OSes and algorithms.
2619 :param dburl: path to the temporary abbreviated db to create
2620 :param lp: a loadparm object.
2621 :param cred: a Credentials object.
2622 :param ldif_file: path to the ldif file to import
2623 :param forced_local_dsa: perform KCC from this DSA's point of view
2624 :return: zero on success, 1 on error
2626 try:
2627 self.samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif_file,
2628 forced_local_dsa)
2629 except ldif_import_export.LdifError, e:
2630 logger.critical(e)
2631 return 1
2632 return 0
2634 def export_ldif(self, dburl, lp, creds, ldif_file):
2635 """Save KCC relevant details to an ldif file
2637 The point of this function is to allow a programmer/debugger to
2638 extract an LDIF file with non-security relevent information from
2639 a DC database. The LDIF file can then be used to "import" via
2640 the import_ldif() function this file into a temporary abbreviated
2641 database. The KCC algorithm can then run against this abbreviated
2642 database for debug or test verification that the topology generated
2643 is computationally the same between different OSes and algorithms.
2645 :param dburl: LDAP database URL to extract info from
2646 :param lp: a loadparm object.
2647 :param cred: a Credentials object.
2648 :param ldif_file: output LDIF file name to create
2649 :return: zero on success, 1 on error
2651 try:
2652 ldif_import_export.samdb_to_ldif_file(self.samdb, dburl, lp, creds,
2653 ldif_file)
2654 except ldif_import_export.LdifError, e:
2655 logger.critical(e)
2656 return 1
2657 return 0