.gitlab-ci-main.yml: Add safe.directory '*'
[Samba.git] / python / samba / kcc / __init__.py
blob22590d0c6c57c2101e99683be679cfa93721f584
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
24 from functools import cmp_to_key
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
47 from samba.common import cmp
50 def sort_dsa_by_gc_and_guid(dsa1, dsa2):
51 """Helper to sort DSAs by guid global catalog status
53 GC DSAs come before non-GC DSAs, other than that, the guids are
54 sorted in NDR form.
56 :param dsa1: A DSA object
57 :param dsa2: Another DSA
58 :return: -1, 0, or 1, indicating sort order.
59 """
60 if dsa1.is_gc() and not dsa2.is_gc():
61 return -1
62 if not dsa1.is_gc() and dsa2.is_gc():
63 return +1
64 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
67 def is_smtp_replication_available():
68 """Can the KCC use SMTP replication?
70 Currently always returns false because Samba doesn't implement
71 SMTP transfer for NC changes between DCs.
73 :return: Boolean (always False)
74 """
75 return False
78 class KCC(object):
79 """The Knowledge Consistency Checker class.
81 A container for objects and methods allowing a run of the KCC. Produces a
82 set of connections in the samdb for which the Distributed Replication
83 Service can then utilize to replicate naming contexts
85 :param unix_now: The putative current time in seconds since 1970.
86 :param readonly: Don't write to the database.
87 :param verify: Check topological invariants for the generated graphs
88 :param debug: Write verbosely to stderr.
89 :param dot_file_dir: write diagnostic Graphviz files in this directory
90 """
91 def __init__(self, unix_now, readonly=False, verify=False, debug=False,
92 dot_file_dir=None):
93 """Initializes the partitions class which can hold
94 our local DCs partitions or all the partitions in
95 the forest
96 """
97 self.part_table = {} # partition objects
98 self.site_table = {}
99 self.ip_transport = None
100 self.sitelink_table = {}
101 self.dsa_by_dnstr = {}
102 self.dsa_by_guid = {}
104 self.get_dsa_by_guidstr = self.dsa_by_guid.get
105 self.get_dsa = self.dsa_by_dnstr.get
107 # TODO: These should be backed by a 'permanent' store so that when
108 # calling DRSGetReplInfo with DS_REPL_INFO_KCC_DSA_CONNECT_FAILURES,
109 # the failure information can be returned
110 self.kcc_failed_links = {}
111 self.kcc_failed_connections = set()
113 # Used in inter-site topology computation. A list
114 # of connections (by NTDSConnection object) that are
115 # to be kept when pruning un-needed NTDS Connections
116 self.kept_connections = set()
118 self.my_dsa_dnstr = None # My dsa DN
119 self.my_dsa = None # My dsa object
121 self.my_site_dnstr = None
122 self.my_site = None
124 self.samdb = None
126 self.unix_now = unix_now
127 self.nt_now = unix2nttime(unix_now)
128 self.readonly = readonly
129 self.verify = verify
130 self.debug = debug
131 self.dot_file_dir = dot_file_dir
133 def load_ip_transport(self):
134 """Loads the inter-site transport objects for Sites
136 :return: None
137 :raise KCCError: if no IP transport is found
139 try:
140 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
141 self.samdb.get_config_basedn(),
142 scope=ldb.SCOPE_SUBTREE,
143 expression="(objectClass=interSiteTransport)")
144 except ldb.LdbError as e2:
145 (enum, estr) = e2.args
146 raise KCCError("Unable to find inter-site transports - (%s)" %
147 estr)
149 for msg in res:
150 dnstr = str(msg.dn)
152 transport = Transport(dnstr)
154 transport.load_transport(self.samdb)
155 if transport.name == 'IP':
156 self.ip_transport = transport
157 elif transport.name == 'SMTP':
158 logger.debug("Samba KCC is ignoring the obsolete "
159 "SMTP transport.")
161 else:
162 logger.warning("Samba KCC does not support the transport "
163 "called %r." % (transport.name,))
165 if self.ip_transport is None:
166 raise KCCError("there doesn't seem to be an IP transport")
168 def load_all_sitelinks(self):
169 """Loads the inter-site siteLink objects
171 :return: None
172 :raise KCCError: if site-links aren't found
174 try:
175 res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
176 self.samdb.get_config_basedn(),
177 scope=ldb.SCOPE_SUBTREE,
178 expression="(objectClass=siteLink)")
179 except ldb.LdbError as e3:
180 (enum, estr) = e3.args
181 raise KCCError("Unable to find inter-site siteLinks - (%s)" % estr)
183 for msg in res:
184 dnstr = str(msg.dn)
186 # already loaded
187 if dnstr in self.sitelink_table:
188 continue
190 sitelink = SiteLink(dnstr)
192 sitelink.load_sitelink(self.samdb)
194 # Assign this siteLink to table
195 # and index by dn
196 self.sitelink_table[dnstr] = sitelink
198 def load_site(self, dn_str):
199 """Helper for load_my_site and load_all_sites.
201 Put all the site's DSAs into the KCC indices.
203 :param dn_str: a site dn_str
204 :return: the Site object pertaining to the dn_str
206 site = Site(dn_str, self.unix_now)
207 site.load_site(self.samdb)
209 # We avoid replacing the site with an identical copy in case
210 # somewhere else has a reference to the old one, which would
211 # lead to all manner of confusion and chaos.
212 guid = str(site.site_guid)
213 if guid not in self.site_table:
214 self.site_table[guid] = site
215 self.dsa_by_dnstr.update(site.dsa_table)
216 self.dsa_by_guid.update((str(x.dsa_guid), x)
217 for x in site.dsa_table.values())
219 return self.site_table[guid]
221 def load_my_site(self):
222 """Load the Site object for the local DSA.
224 :return: None
226 self.my_site_dnstr = ("CN=%s,CN=Sites,%s" % (
227 self.samdb.server_site_name(),
228 self.samdb.get_config_basedn()))
230 self.my_site = self.load_site(self.my_site_dnstr)
232 def load_all_sites(self):
233 """Discover all sites and create Site objects.
235 :return: None
236 :raise: KCCError if sites can't be found
238 try:
239 res = self.samdb.search("CN=Sites,%s" %
240 self.samdb.get_config_basedn(),
241 scope=ldb.SCOPE_SUBTREE,
242 expression="(objectClass=site)")
243 except ldb.LdbError as e4:
244 (enum, estr) = e4.args
245 raise KCCError("Unable to find sites - (%s)" % estr)
247 for msg in res:
248 sitestr = str(msg.dn)
249 self.load_site(sitestr)
251 def load_my_dsa(self):
252 """Discover my nTDSDSA dn thru the rootDSE entry
254 :return: None
255 :raise: KCCError if DSA can't be found
257 dn_query = "<GUID=%s>" % self.samdb.get_ntds_GUID()
258 dn = ldb.Dn(self.samdb, dn_query)
259 try:
260 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
261 attrs=["objectGUID"])
262 except ldb.LdbError as e5:
263 (enum, estr) = e5.args
264 DEBUG_FN("Search for dn '%s' [from %s] failed: %s. "
265 "This typically happens in --importldif mode due "
266 "to lack of module support." % (dn, dn_query, estr))
267 try:
268 # We work around the failure above by looking at the
269 # dsServiceName that was put in the fake rootdse by
270 # the --exportldif, rather than the
271 # samdb.get_ntds_GUID(). The disadvantage is that this
272 # mode requires we modify the @ROOTDSE dnq to support
273 # --forced-local-dsa
274 service_name_res = self.samdb.search(base="",
275 scope=ldb.SCOPE_BASE,
276 attrs=["dsServiceName"])
277 dn = ldb.Dn(self.samdb,
278 service_name_res[0]["dsServiceName"][0].decode('utf8'))
280 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
281 attrs=["objectGUID"])
282 except ldb.LdbError as e:
283 (enum, estr) = e.args
284 raise KCCError("Unable to find my nTDSDSA - (%s)" % estr)
286 if len(res) != 1:
287 raise KCCError("Unable to find my nTDSDSA at %s" %
288 dn.extended_str())
290 ntds_guid = misc.GUID(self.samdb.get_ntds_GUID())
291 if misc.GUID(res[0]["objectGUID"][0]) != ntds_guid:
292 raise KCCError("Did not find the GUID we expected,"
293 " perhaps due to --importldif")
295 self.my_dsa_dnstr = str(res[0].dn)
297 self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
299 if self.my_dsa_dnstr not in self.dsa_by_dnstr:
300 debug.DEBUG_DARK_YELLOW("my_dsa %s isn't in self.dsas_by_dnstr:"
301 " it must be RODC.\n"
302 "Let's add it, because my_dsa is special!"
303 "\n(likewise for self.dsa_by_guid)" %
304 self.my_dsa_dnstr)
306 self.dsa_by_dnstr[self.my_dsa_dnstr] = self.my_dsa
307 self.dsa_by_guid[str(self.my_dsa.dsa_guid)] = self.my_dsa
309 def load_all_partitions(self):
310 """Discover and load all partitions.
312 Each NC is inserted into the part_table by partition
313 dn string (not the nCName dn string)
315 :return: None
316 :raise: KCCError if partitions can't be found
318 try:
319 res = self.samdb.search("CN=Partitions,%s" %
320 self.samdb.get_config_basedn(),
321 scope=ldb.SCOPE_SUBTREE,
322 expression="(objectClass=crossRef)")
323 except ldb.LdbError as e6:
324 (enum, estr) = e6.args
325 raise KCCError("Unable to find partitions - (%s)" % estr)
327 for msg in res:
328 partstr = str(msg.dn)
330 # already loaded
331 if partstr in self.part_table:
332 continue
334 part = Partition(partstr)
336 part.load_partition(self.samdb)
337 self.part_table[partstr] = part
339 def refresh_failed_links_connections(self, ping=None):
340 """Ensure the failed links list is up to date
342 Based on MS-ADTS 6.2.2.1
344 :param ping: An oracle function of remote site availability
345 :return: None
347 # LINKS: Refresh failed links
348 self.kcc_failed_links = {}
349 current, needed = self.my_dsa.get_rep_tables()
350 for replica in current.values():
351 # For every possible connection to replicate
352 for reps_from in replica.rep_repsFrom:
353 failure_count = reps_from.consecutive_sync_failures
354 if failure_count <= 0:
355 continue
357 dsa_guid = str(reps_from.source_dsa_obj_guid)
358 time_first_failure = reps_from.last_success
359 last_result = reps_from.last_attempt
360 dns_name = reps_from.dns_name1
362 f = self.kcc_failed_links.get(dsa_guid)
363 if f is None:
364 f = KCCFailedObject(dsa_guid, failure_count,
365 time_first_failure, last_result,
366 dns_name)
367 self.kcc_failed_links[dsa_guid] = f
368 else:
369 f.failure_count = max(f.failure_count, failure_count)
370 f.time_first_failure = min(f.time_first_failure,
371 time_first_failure)
372 f.last_result = last_result
374 # CONNECTIONS: Refresh failed connections
375 restore_connections = set()
376 if ping is not None:
377 DEBUG("refresh_failed_links: checking if links are still down")
378 for connection in self.kcc_failed_connections:
379 if ping(connection.dns_name):
380 # Failed connection is no longer failing
381 restore_connections.add(connection)
382 else:
383 connection.failure_count += 1
384 else:
385 DEBUG("refresh_failed_links: not checking live links because we\n"
386 "weren't asked to --attempt-live-connections")
388 # Remove the restored connections from the failed connections
389 self.kcc_failed_connections.difference_update(restore_connections)
391 def is_stale_link_connection(self, target_dsa):
392 """Check whether a link to a remote DSA is stale
394 Used in MS-ADTS 6.2.2.2 Intrasite Connection Creation
396 Returns True if the remote seems to have been down for at
397 least two hours, otherwise False.
399 :param target_dsa: the remote DSA object
400 :return: True if link is stale, otherwise False
402 failed_link = self.kcc_failed_links.get(str(target_dsa.dsa_guid))
403 if failed_link:
404 # failure_count should be > 0, but check anyways
405 if failed_link.failure_count > 0:
406 unix_first_failure = \
407 nttime2unix(failed_link.time_first_failure)
408 # TODO guard against future
409 if unix_first_failure > self.unix_now:
410 logger.error("The last success time attribute for "
411 "repsFrom is in the future!")
413 # Perform calculation in seconds
414 if (self.unix_now - unix_first_failure) > 60 * 60 * 2:
415 return True
417 # TODO connections.
418 # We have checked failed *links*, but we also need to check
419 # *connections*
421 return False
423 # TODO: This should be backed by some form of local database
424 def remove_unneeded_failed_links_connections(self):
425 # Remove all tuples in kcc_failed_links where failure count = 0
426 # In this implementation, this should never happen.
428 # Remove all connections which were not used this run or connections
429 # that became active during this run.
430 pass
432 def _ensure_connections_are_loaded(self, connections):
433 """Load or fake-load NTDSConnections lacking GUIDs
435 New connections don't have GUIDs and created times which are
436 needed for sorting. If we're in read-only mode, we make fake
437 GUIDs, otherwise we ask SamDB to do it for us.
439 :param connections: an iterable of NTDSConnection objects.
440 :return: None
442 for cn_conn in connections:
443 if cn_conn.guid is None:
444 if self.readonly:
445 cn_conn.guid = misc.GUID(str(uuid.uuid4()))
446 cn_conn.whenCreated = self.nt_now
447 else:
448 cn_conn.load_connection(self.samdb)
450 def _mark_broken_ntdsconn(self):
451 """Find NTDS Connections that lack a remote
453 I'm not sure how they appear. Let's be rid of them by marking
454 them with the to_be_deleted attribute.
456 :return: None
458 for cn_conn in self.my_dsa.connect_table.values():
459 s_dnstr = cn_conn.get_from_dnstr()
460 if s_dnstr is None:
461 DEBUG_FN("%s has phantom connection %s" % (self.my_dsa,
462 cn_conn))
463 cn_conn.to_be_deleted = True
465 def _mark_unneeded_local_ntdsconn(self):
466 """Find unneeded intrasite NTDS Connections for removal
468 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections.
469 Every DC removes its own unnecessary intrasite connections.
470 This function tags them with the to_be_deleted attribute.
472 :return: None
474 # XXX should an RODC be regarded as same site? It isn't part
475 # of the intrasite ring.
477 if self.my_site.is_cleanup_ntdsconn_disabled():
478 DEBUG_FN("not doing ntdsconn cleanup for site %s, "
479 "because it is disabled" % self.my_site)
480 return
482 mydsa = self.my_dsa
484 try:
485 self._ensure_connections_are_loaded(mydsa.connect_table.values())
486 except KCCError:
487 # RODC never actually added any connections to begin with
488 if mydsa.is_ro():
489 return
491 local_connections = []
493 for cn_conn in mydsa.connect_table.values():
494 s_dnstr = cn_conn.get_from_dnstr()
495 if s_dnstr in self.my_site.dsa_table:
496 removable = not (cn_conn.is_generated() or
497 cn_conn.is_rodc_topology())
498 packed_guid = ndr_pack(cn_conn.guid)
499 local_connections.append((cn_conn, s_dnstr,
500 packed_guid, removable))
502 # Avoid "ValueError: r cannot be bigger than the iterable" in
503 # for a, b in itertools.permutations(local_connections, 2):
504 if (len(local_connections) < 2):
505 return
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 ISTG, 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 or '\\0ADEL' in s_dnstr:
545 logger.info("DSA appears deleted, removing connection %s"
546 % s_dnstr)
547 cn.to_be_deleted = True
548 continue
549 connections_and_dsas.append((cn, dsa, from_dsa))
551 self._ensure_connections_are_loaded(x[0] for x in connections_and_dsas)
552 for cn, to_dsa, from_dsa in connections_and_dsas:
553 if not cn.is_generated() or cn.is_rodc_topology():
554 continue
556 # If the connection is in the kept_connections list, we
557 # only remove it if an endpoint seems down.
558 if (cn in self.kept_connections and
559 not (self.is_bridgehead_failed(to_dsa, True) or
560 self.is_bridgehead_failed(from_dsa, True))):
561 continue
563 # this one is broken and might be superseded by another.
564 # But which other? Let's just say another link to the same
565 # site can supersede.
566 from_dnstr = from_dsa.dsa_dnstr
567 for site in self.site_table.values():
568 if from_dnstr in site.rw_dsa_table:
569 for cn2, to_dsa2, from_dsa2 in connections_and_dsas:
570 if (cn is not cn2 and
571 from_dsa2 in site.rw_dsa_table):
572 cn.to_be_deleted = True
574 def _commit_changes(self, dsa):
575 if dsa.is_ro() or self.readonly:
576 for connect in dsa.connect_table.values():
577 if connect.to_be_deleted:
578 logger.info("TO BE DELETED:\n%s" % connect)
579 if connect.to_be_added:
580 logger.info("TO BE ADDED:\n%s" % connect)
581 if connect.to_be_modified:
582 logger.info("TO BE MODIFIED:\n%s" % connect)
584 # Perform deletion from our tables but perform
585 # no database modification
586 dsa.commit_connections(self.samdb, ro=True)
587 else:
588 # Commit any modified connections
589 dsa.commit_connections(self.samdb)
591 def remove_unneeded_ntdsconn(self, all_connected):
592 """Remove unneeded NTDS Connections once topology is calculated
594 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections
596 :param all_connected: indicates whether all sites are connected
597 :return: None
599 self._mark_broken_ntdsconn()
600 self._mark_unneeded_local_ntdsconn()
601 # if we are not the istg, we're done!
602 # if we are the istg, but all_connected is False, we also do nothing.
603 if self.my_dsa.is_istg() and all_connected:
604 self._mark_unneeded_intersite_ntdsconn()
606 for dsa in self.my_site.dsa_table.values():
607 self._commit_changes(dsa)
609 def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
610 """Update an repsFrom object if required.
612 Part of MS-ADTS 6.2.2.5.
614 Update t_repsFrom if necessary to satisfy requirements. Such
615 updates are typically required when the IDL_DRSGetNCChanges
616 server has moved from one site to another--for example, to
617 enable compression when the server is moved from the
618 client's site to another site.
620 The repsFrom.update_flags bit field may be modified
621 auto-magically if any changes are made here. See
622 kcc_utils.RepsFromTo for gory details.
625 :param n_rep: NC replica we need
626 :param t_repsFrom: repsFrom tuple to modify
627 :param s_rep: NC replica at source DSA
628 :param s_dsa: source DSA
629 :param cn_conn: Local DSA NTDSConnection child
631 :return: None
633 s_dnstr = s_dsa.dsa_dnstr
634 same_site = s_dnstr in self.my_site.dsa_table
636 # if schedule doesn't match then update and modify
637 times = convert_schedule_to_repltimes(cn_conn.schedule)
638 if times != t_repsFrom.schedule:
639 t_repsFrom.schedule = times
641 # Bit DRS_ADD_REF is set in replicaFlags unconditionally
642 # Samba ONLY:
643 if ((t_repsFrom.replica_flags &
644 drsuapi.DRSUAPI_DRS_ADD_REF) == 0x0):
645 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_ADD_REF
647 # Bit DRS_PER_SYNC is set in replicaFlags if and only
648 # if nTDSConnection schedule has a value v that specifies
649 # scheduled replication is to be performed at least once
650 # per week.
651 if cn_conn.is_schedule_minimum_once_per_week():
653 if ((t_repsFrom.replica_flags &
654 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0):
655 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
657 # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
658 # if the source DSA and the local DC's nTDSDSA object are
659 # in the same site or source dsa is the FSMO role owner
660 # of one or more FSMO roles in the NC replica.
661 if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
663 if ((t_repsFrom.replica_flags &
664 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0):
665 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
667 # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
668 # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
669 # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
670 # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
671 # t.replicaFlags if and only if s and the local DC's
672 # nTDSDSA object are in different sites.
673 if ((cn_conn.options &
674 dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0):
676 if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
677 # WARNING
679 # it LOOKS as if this next test is a bit silly: it
680 # checks the flag then sets it if it not set; the same
681 # effect could be achieved by unconditionally setting
682 # it. But in fact the repsFrom object has special
683 # magic attached to it, and altering replica_flags has
684 # side-effects. That is bad in my opinion, but there
685 # you go.
686 if ((t_repsFrom.replica_flags &
687 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
688 t_repsFrom.replica_flags |= \
689 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
691 elif not same_site:
693 if ((t_repsFrom.replica_flags &
694 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
695 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
697 # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
698 # and only if s and the local DC's nTDSDSA object are
699 # not in the same site and the
700 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
701 # clear in cn!options
702 if (not same_site and
703 (cn_conn.options &
704 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
706 if ((t_repsFrom.replica_flags &
707 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0):
708 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
710 # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
711 # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
712 if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
714 if ((t_repsFrom.replica_flags &
715 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0):
716 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
718 # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
719 # set in t.replicaFlags if and only if cn!enabledConnection = false.
720 if not cn_conn.is_enabled():
722 if ((t_repsFrom.replica_flags &
723 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0):
724 t_repsFrom.replica_flags |= \
725 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
727 if ((t_repsFrom.replica_flags &
728 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0):
729 t_repsFrom.replica_flags |= \
730 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
732 # If s and the local DC's nTDSDSA object are in the same site,
733 # cn!transportType has no value, or the RDN of cn!transportType
734 # is CN=IP:
736 # Bit DRS_MAIL_REP in t.replicaFlags is clear.
738 # t.uuidTransport = NULL GUID.
740 # t.uuidDsa = The GUID-based DNS name of s.
742 # Otherwise:
744 # Bit DRS_MAIL_REP in t.replicaFlags is set.
746 # If x is the object with dsname cn!transportType,
747 # t.uuidTransport = x!objectGUID.
749 # Let a be the attribute identified by
750 # x!transportAddressAttribute. If a is
751 # the dNSHostName attribute, t.uuidDsa = the GUID-based
752 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
754 # It appears that the first statement i.e.
756 # "If s and the local DC's nTDSDSA object are in the same
757 # site, cn!transportType has no value, or the RDN of
758 # cn!transportType is CN=IP:"
760 # could be a slightly tighter statement if it had an "or"
761 # between each condition. I believe this should
762 # be interpreted as:
764 # IF (same-site) OR (no-value) OR (type-ip)
766 # because IP should be the primary transport mechanism
767 # (even in inter-site) and the absence of the transportType
768 # attribute should always imply IP no matter if its multi-site
770 # NOTE MS-TECH INCORRECT:
772 # All indications point to these statements above being
773 # incorrectly stated:
775 # t.uuidDsa = The GUID-based DNS name of s.
777 # Let a be the attribute identified by
778 # x!transportAddressAttribute. If a is
779 # the dNSHostName attribute, t.uuidDsa = the GUID-based
780 # DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
782 # because the uuidDSA is a GUID and not a GUID-base DNS
783 # name. Nor can uuidDsa hold (s!parent)!a if not
784 # dNSHostName. What should have been said is:
786 # t.naDsa = The GUID-based DNS name of s
788 # That would also be correct if transportAddressAttribute
789 # were "mailAddress" because (naDsa) can also correctly
790 # hold the SMTP ISM service address.
792 nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
794 if ((t_repsFrom.replica_flags &
795 drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0):
796 t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
798 t_repsFrom.transport_guid = misc.GUID()
800 # See (NOTE MS-TECH INCORRECT) above
802 # NOTE: it looks like these conditionals are pointless,
803 # because the state will end up as `t_repsFrom.dns_name1 ==
804 # nastr` in either case, BUT the repsFrom thing is magic and
805 # assigning to it alters some flags. So we try not to update
806 # it unless necessary.
807 if t_repsFrom.dns_name1 != nastr:
808 t_repsFrom.dns_name1 = nastr
810 if t_repsFrom.version > 0x1 and t_repsFrom.dns_name2 != nastr:
811 t_repsFrom.dns_name2 = nastr
813 if t_repsFrom.is_modified():
814 DEBUG_FN("modify_repsFrom(): %s" % t_repsFrom)
816 def get_dsa_for_implied_replica(self, n_rep, cn_conn):
817 """If a connection imply a replica, find the relevant DSA
819 Given a NC replica and NTDS Connection, determine if the
820 connection implies a repsFrom tuple should be present from the
821 source DSA listed in the connection to the naming context. If
822 it should be, return the DSA; otherwise return None.
824 Based on part of MS-ADTS 6.2.2.5
826 :param n_rep: NC replica
827 :param cn_conn: NTDS Connection
828 :return: source DSA or None
830 # XXX different conditions for "implies" than MS-ADTS 6.2.2
831 # preamble.
833 # It boils down to: we want an enabled, non-FRS connections to
834 # a valid remote DSA with a non-RO replica corresponding to
835 # n_rep.
837 if not cn_conn.is_enabled() or cn_conn.is_rodc_topology():
838 return None
840 s_dnstr = cn_conn.get_from_dnstr()
841 s_dsa = self.get_dsa(s_dnstr)
843 # No DSA matching this source DN string?
844 if s_dsa is None:
845 return None
847 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
849 if (s_rep is not None and
850 s_rep.is_present() and
851 (not s_rep.is_ro() or n_rep.is_partial())):
852 return s_dsa
853 return None
855 def translate_ntdsconn(self, current_dsa=None):
856 """Adjust repsFrom to match NTDSConnections
858 This function adjusts values of repsFrom abstract attributes of NC
859 replicas on the local DC to match those implied by
860 nTDSConnection objects.
862 Based on [MS-ADTS] 6.2.2.5
864 :param current_dsa: optional DSA on whose behalf we are acting.
865 :return: None
867 ro = False
868 if current_dsa is None:
869 current_dsa = self.my_dsa
871 if current_dsa.is_ro():
872 ro = True
874 if current_dsa.is_translate_ntdsconn_disabled():
875 DEBUG_FN("skipping translate_ntdsconn() "
876 "because disabling flag is set")
877 return
879 DEBUG_FN("translate_ntdsconn(): enter")
881 current_rep_table, needed_rep_table = current_dsa.get_rep_tables()
883 # Filled in with replicas we currently have that need deleting
884 delete_reps = set()
886 # We're using the MS notation names here to allow
887 # correlation back to the published algorithm.
889 # n_rep - NC replica (n)
890 # t_repsFrom - tuple (t) in n!repsFrom
891 # s_dsa - Source DSA of the replica. Defined as nTDSDSA
892 # object (s) such that (s!objectGUID = t.uuidDsa)
893 # In our IDL representation of repsFrom the (uuidDsa)
894 # attribute is called (source_dsa_obj_guid)
895 # cn_conn - (cn) is nTDSConnection object and child of the local
896 # DC's nTDSDSA object and (cn!fromServer = s)
897 # s_rep - source DSA replica of n
899 # If we have the replica and its not needed
900 # then we add it to the "to be deleted" list.
901 for dnstr in current_rep_table:
902 # If we're on the RODC, hardcode the update flags
903 if ro:
904 c_rep = current_rep_table[dnstr]
905 c_rep.load_repsFrom(self.samdb)
906 for t_repsFrom in c_rep.rep_repsFrom:
907 replica_flags = (drsuapi.DRSUAPI_DRS_INIT_SYNC |
908 drsuapi.DRSUAPI_DRS_PER_SYNC |
909 drsuapi.DRSUAPI_DRS_ADD_REF |
910 drsuapi.DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING |
911 drsuapi.DRSUAPI_DRS_NONGC_RO_REP)
912 if t_repsFrom.replica_flags != replica_flags:
913 t_repsFrom.replica_flags = replica_flags
914 c_rep.commit_repsFrom(self.samdb, ro=self.readonly)
915 else:
916 if dnstr not in needed_rep_table:
917 delete_reps.add(dnstr)
919 DEBUG_FN('current %d needed %d delete %d' % (len(current_rep_table),
920 len(needed_rep_table), len(delete_reps)))
922 if delete_reps:
923 # TODO Must delete repsFrom/repsTo for these replicas
924 DEBUG('deleting these reps: %s' % delete_reps)
925 for dnstr in delete_reps:
926 del current_rep_table[dnstr]
928 # HANDLE REPS-FROM
930 # Now perform the scan of replicas we'll need
931 # and compare any current repsFrom against the
932 # connections
933 for n_rep in needed_rep_table.values():
935 # load any repsFrom and fsmo roles as we'll
936 # need them during connection translation
937 n_rep.load_repsFrom(self.samdb)
938 n_rep.load_fsmo_roles(self.samdb)
940 # Loop thru the existing repsFrom tuples (if any)
941 # XXX This is a list and could contain duplicates
942 # (multiple load_repsFrom calls)
943 for t_repsFrom in n_rep.rep_repsFrom:
945 # for each tuple t in n!repsFrom, let s be the nTDSDSA
946 # object such that s!objectGUID = t.uuidDsa
947 guidstr = str(t_repsFrom.source_dsa_obj_guid)
948 s_dsa = self.get_dsa_by_guidstr(guidstr)
950 # Source dsa is gone from config (strange)
951 # so cleanup stale repsFrom for unlisted DSA
952 if s_dsa is None:
953 logger.warning("repsFrom source DSA guid (%s) not found" %
954 guidstr)
955 t_repsFrom.to_be_deleted = True
956 continue
958 # Find the connection that this repsFrom would use. If
959 # there isn't a good one (i.e. non-RODC_TOPOLOGY,
960 # meaning non-FRS), we delete the repsFrom.
961 s_dnstr = s_dsa.dsa_dnstr
962 connections = current_dsa.get_connection_by_from_dnstr(s_dnstr)
963 for cn_conn in connections:
964 if not cn_conn.is_rodc_topology():
965 break
966 else:
967 # no break means no non-rodc_topology connection exists
968 t_repsFrom.to_be_deleted = True
969 continue
971 # KCC removes this repsFrom tuple if any of the following
972 # is true:
973 # No NC replica of the NC "is present" on DSA that
974 # would be source of replica
976 # A writable replica of the NC "should be present" on
977 # the local DC, but a partial replica "is present" on
978 # the source DSA
979 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
981 if s_rep is None or not s_rep.is_present() or \
982 (not n_rep.is_ro() and s_rep.is_partial()):
984 t_repsFrom.to_be_deleted = True
985 continue
987 # If the KCC did not remove t from n!repsFrom, it updates t
988 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
990 # Loop thru connections and add implied repsFrom tuples
991 # for each NTDSConnection under our local DSA if the
992 # repsFrom is not already present
993 for cn_conn in current_dsa.connect_table.values():
995 s_dsa = self.get_dsa_for_implied_replica(n_rep, cn_conn)
996 if s_dsa is None:
997 continue
999 # Loop thru the existing repsFrom tuples (if any) and
1000 # if we already have a tuple for this connection then
1001 # no need to proceed to add. It will have been changed
1002 # to have the correct attributes above
1003 for t_repsFrom in n_rep.rep_repsFrom:
1004 guidstr = str(t_repsFrom.source_dsa_obj_guid)
1005 if s_dsa is self.get_dsa_by_guidstr(guidstr):
1006 s_dsa = None
1007 break
1009 if s_dsa is None:
1010 continue
1012 # Create a new RepsFromTo and proceed to modify
1013 # it according to specification
1014 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
1016 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
1018 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
1020 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
1022 # Add to our NC repsFrom as this is newly computed
1023 if t_repsFrom.is_modified():
1024 n_rep.rep_repsFrom.append(t_repsFrom)
1026 if self.readonly or ro:
1027 # Display any to be deleted or modified repsFrom
1028 text = n_rep.dumpstr_to_be_deleted()
1029 if text:
1030 logger.info("TO BE DELETED:\n%s" % text)
1031 text = n_rep.dumpstr_to_be_modified()
1032 if text:
1033 logger.info("TO BE MODIFIED:\n%s" % text)
1035 # Perform deletion from our tables but perform
1036 # no database modification
1037 n_rep.commit_repsFrom(self.samdb, ro=True)
1038 else:
1039 # Commit any modified repsFrom to the NC replica
1040 n_rep.commit_repsFrom(self.samdb)
1042 # HANDLE REPS-TO:
1044 # Now perform the scan of replicas we'll need
1045 # and compare any current repsTo against the
1046 # connections
1048 # RODC should never push to anybody (should we check this?)
1049 if ro:
1050 return
1052 for n_rep in needed_rep_table.values():
1054 # load any repsTo and fsmo roles as we'll
1055 # need them during connection translation
1056 n_rep.load_repsTo(self.samdb)
1058 # Loop thru the existing repsTo tuples (if any)
1059 # XXX This is a list and could contain duplicates
1060 # (multiple load_repsTo calls)
1061 for t_repsTo in n_rep.rep_repsTo:
1063 # for each tuple t in n!repsTo, let s be the nTDSDSA
1064 # object such that s!objectGUID = t.uuidDsa
1065 guidstr = str(t_repsTo.source_dsa_obj_guid)
1066 s_dsa = self.get_dsa_by_guidstr(guidstr)
1068 # Source dsa is gone from config (strange)
1069 # so cleanup stale repsTo for unlisted DSA
1070 if s_dsa is None:
1071 logger.warning("repsTo source DSA guid (%s) not found" %
1072 guidstr)
1073 t_repsTo.to_be_deleted = True
1074 continue
1076 # Find the connection that this repsTo would use. If
1077 # there isn't a good one (i.e. non-RODC_TOPOLOGY,
1078 # meaning non-FRS), we delete the repsTo.
1079 s_dnstr = s_dsa.dsa_dnstr
1080 if '\\0ADEL' in s_dnstr:
1081 logger.warning("repsTo source DSA guid (%s) appears deleted" %
1082 guidstr)
1083 t_repsTo.to_be_deleted = True
1084 continue
1086 connections = s_dsa.get_connection_by_from_dnstr(self.my_dsa_dnstr)
1087 if len(connections) > 0:
1088 # Then this repsTo is tentatively valid
1089 continue
1090 else:
1091 # There is no plausible connection for this repsTo
1092 t_repsTo.to_be_deleted = True
1094 if self.readonly:
1095 # Display any to be deleted or modified repsTo
1096 for rt in n_rep.rep_repsTo:
1097 if rt.to_be_deleted:
1098 logger.info("REMOVING REPS-TO: %s" % rt)
1100 # Perform deletion from our tables but perform
1101 # no database modification
1102 n_rep.commit_repsTo(self.samdb, ro=True)
1103 else:
1104 # Commit any modified repsTo to the NC replica
1105 n_rep.commit_repsTo(self.samdb)
1107 # TODO Remove any duplicate repsTo values. This should never happen in
1108 # any normal situations.
1110 def merge_failed_links(self, ping=None):
1111 """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
1113 The KCC on a writable DC attempts to merge the link and connection
1114 failure information from bridgehead DCs in its own site to help it
1115 identify failed bridgehead DCs.
1117 Based on MS-ADTS 6.2.2.3.2 "Merge of kCCFailedLinks and kCCFailedLinks
1118 from Bridgeheads"
1120 :param ping: An oracle of current bridgehead availability
1121 :return: None
1123 # 1. Queries every bridgehead server in your site (other than yourself)
1124 # 2. For every ntDSConnection that references a server in a different
1125 # site merge all the failure info
1127 # XXX - not implemented yet
1128 if ping is not None:
1129 debug.DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1130 else:
1131 DEBUG_FN("skipping merge_failed_links() because it requires "
1132 "real network connections\n"
1133 "and we weren't asked to --attempt-live-connections")
1135 def setup_graph(self, part):
1136 """Set up an intersite graph
1138 An intersite graph has a Vertex for each site object, a
1139 MultiEdge for each SiteLink object, and a MutliEdgeSet for
1140 each siteLinkBridge object (or implied siteLinkBridge). It
1141 reflects the intersite topology in a slightly more abstract
1142 graph form.
1144 Roughly corresponds to MS-ADTS 6.2.2.3.4.3
1146 :param part: a Partition object
1147 :returns: an InterSiteGraph object
1149 # If 'Bridge all site links' is enabled and Win2k3 bridges required
1150 # is not set
1151 # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1152 # No documentation for this however, ntdsapi.h appears to have:
1153 # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1154 bridges_required = self.my_site.site_options & 0x00001002 != 0
1155 transport_guid = str(self.ip_transport.guid)
1157 g = setup_graph(part, self.site_table, transport_guid,
1158 self.sitelink_table, bridges_required)
1160 if self.verify or self.dot_file_dir is not None:
1161 dot_edges = []
1162 for edge in g.edges:
1163 for a, b in itertools.combinations(edge.vertices, 2):
1164 dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1165 verify_properties = ()
1166 name = 'site_edges_%s' % part.partstr
1167 verify_and_dot(name, dot_edges, directed=False,
1168 label=self.my_dsa_dnstr,
1169 properties=verify_properties, debug=DEBUG,
1170 verify=self.verify,
1171 dot_file_dir=self.dot_file_dir)
1173 return g
1175 def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1176 """Get a bridghead DC for a site.
1178 Part of MS-ADTS 6.2.2.3.4.4
1180 :param site: site object representing for which a bridgehead
1181 DC is desired.
1182 :param part: crossRef for NC to replicate.
1183 :param transport: interSiteTransport object for replication
1184 traffic.
1185 :param partial_ok: True if a DC containing a partial
1186 replica or a full replica will suffice, False if only
1187 a full replica will suffice.
1188 :param detect_failed: True to detect failed DCs and route
1189 replication traffic around them, False to assume no DC
1190 has failed.
1191 :return: dsa object for the bridgehead DC or None
1194 bhs = self.get_all_bridgeheads(site, part, transport,
1195 partial_ok, detect_failed)
1196 if not bhs:
1197 debug.DEBUG_MAGENTA("get_bridgehead FAILED:\nsitedn = %s" %
1198 site.site_dnstr)
1199 return None
1201 debug.DEBUG_GREEN("get_bridgehead:\n\tsitedn = %s\n\tbhdn = %s" %
1202 (site.site_dnstr, bhs[0].dsa_dnstr))
1203 return bhs[0]
1205 def get_all_bridgeheads(self, site, part, transport,
1206 partial_ok, detect_failed):
1207 """Get all bridghead DCs on a site satisfying the given criteria
1209 Part of MS-ADTS 6.2.2.3.4.4
1211 :param site: site object representing the site for which
1212 bridgehead DCs are desired.
1213 :param part: partition for NC to replicate.
1214 :param transport: interSiteTransport object for
1215 replication traffic.
1216 :param partial_ok: True if a DC containing a partial
1217 replica or a full replica will suffice, False if
1218 only a full replica will suffice.
1219 :param detect_failed: True to detect failed DCs and route
1220 replication traffic around them, FALSE to assume
1221 no DC has failed.
1222 :return: list of dsa object for available bridgehead DCs
1224 bhs = []
1226 if transport.name != "IP":
1227 raise KCCError("get_all_bridgeheads has run into a "
1228 "non-IP transport! %r"
1229 % (transport.name,))
1231 DEBUG_FN(site.rw_dsa_table)
1232 for dsa in site.rw_dsa_table.values():
1234 pdnstr = dsa.get_parent_dnstr()
1236 # IF t!bridgeheadServerListBL has one or more values and
1237 # t!bridgeheadServerListBL does not contain a reference
1238 # to the parent object of dc then skip dc
1239 if ((len(transport.bridgehead_list) != 0 and
1240 pdnstr not in transport.bridgehead_list)):
1241 continue
1243 # IF dc is in the same site as the local DC
1244 # IF a replica of cr!nCName is not in the set of NC replicas
1245 # that "should be present" on dc or a partial replica of the
1246 # NC "should be present" but partialReplicasOkay = FALSE
1247 # Skip dc
1248 if self.my_site.same_site(dsa):
1249 needed, ro, partial = part.should_be_present(dsa)
1250 if not needed or (partial and not partial_ok):
1251 continue
1252 rep = dsa.get_current_replica(part.nc_dnstr)
1254 # ELSE
1255 # IF an NC replica of cr!nCName is not in the set of NC
1256 # replicas that "are present" on dc or a partial replica of
1257 # the NC "is present" but partialReplicasOkay = FALSE
1258 # Skip dc
1259 else:
1260 rep = dsa.get_current_replica(part.nc_dnstr)
1261 if rep is None or (rep.is_partial() and not partial_ok):
1262 continue
1264 # IF AmIRODC() and cr!nCName corresponds to default NC then
1265 # Let dsaobj be the nTDSDSA object of the dc
1266 # IF dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1267 # Skip dc
1268 if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1269 if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1270 continue
1272 # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1273 # Skip dc
1274 if self.is_bridgehead_failed(dsa, detect_failed):
1275 DEBUG("bridgehead is failed")
1276 continue
1278 DEBUG_FN("found a bridgehead: %s" % dsa.dsa_dnstr)
1279 bhs.append(dsa)
1281 # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1282 # s!options
1283 # SORT bhs such that all GC servers precede DCs that are not GC
1284 # servers, and otherwise by ascending objectGUID
1285 # ELSE
1286 # SORT bhs in a random order
1287 if site.is_random_bridgehead_disabled():
1288 bhs.sort(key=cmp_to_key(sort_dsa_by_gc_and_guid))
1289 else:
1290 random.shuffle(bhs)
1291 debug.DEBUG_YELLOW(bhs)
1292 return bhs
1294 def is_bridgehead_failed(self, dsa, detect_failed):
1295 """Determine whether a given DC is known to be in a failed state
1297 :param dsa: the bridgehead to test
1298 :param detect_failed: True to really check, False to assume no failure
1299 :return: True if and only if the DC should be considered failed
1301 Here we DEPART from the pseudo code spec which appears to be
1302 wrong. It says, in full:
1304 /***** BridgeheadDCFailed *****/
1305 /* Determine whether a given DC is known to be in a failed state.
1306 * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1307 * IN: detectFailedDCs - TRUE if and only failed DC detection is
1308 * enabled.
1309 * RETURNS: TRUE if and only if the DC should be considered to be in a
1310 * failed state.
1312 BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1314 IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1315 the options attribute of the site settings object for the local
1316 DC's site
1317 RETURN FALSE
1318 ELSEIF a tuple z exists in the kCCFailedLinks or
1319 kCCFailedConnections variables such that z.UUIDDsa =
1320 objectGUID, z.FailureCount > 1, and the current time -
1321 z.TimeFirstFailure > 2 hours
1322 RETURN TRUE
1323 ELSE
1324 RETURN detectFailedDCs
1325 ENDIF
1328 where you will see detectFailedDCs is not behaving as
1329 advertised -- it is acting as a default return code in the
1330 event that a failure is not detected, not a switch turning
1331 detection on or off. Elsewhere the documentation seems to
1332 concur with the comment rather than the code.
1334 if not detect_failed:
1335 return False
1337 # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1338 # When DETECT_STALE_DISABLED, we can never know of if
1339 # it's in a failed state
1340 if self.my_site.site_options & 0x00000008:
1341 return False
1343 return self.is_stale_link_connection(dsa)
1345 def create_connection(self, part, rbh, rsite, transport,
1346 lbh, lsite, link_opt, link_sched,
1347 partial_ok, detect_failed):
1348 """Create an nTDSConnection object as specified if it doesn't exist.
1350 Part of MS-ADTS 6.2.2.3.4.5
1352 :param part: crossRef object for the NC to replicate.
1353 :param rbh: nTDSDSA object for DC to act as the
1354 IDL_DRSGetNCChanges server (which is in a site other
1355 than the local DC's site).
1356 :param rsite: site of the rbh
1357 :param transport: interSiteTransport object for the transport
1358 to use for replication traffic.
1359 :param lbh: nTDSDSA object for DC to act as the
1360 IDL_DRSGetNCChanges client (which is in the local DC's site).
1361 :param lsite: site of the lbh
1362 :param link_opt: Replication parameters (aggregated siteLink options,
1363 etc.)
1364 :param link_sched: Schedule specifying the times at which
1365 to begin replicating.
1366 :partial_ok: True if bridgehead DCs containing partial
1367 replicas of the NC are acceptable.
1368 :param detect_failed: True to detect failed DCs and route
1369 replication traffic around them, FALSE to assume no DC
1370 has failed.
1372 rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1373 partial_ok, False)
1374 rbh_table = dict((x.dsa_dnstr, x) for x in rbhs_all)
1376 debug.DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all),
1377 [x.dsa_dnstr for x in rbhs_all]))
1379 # MS-TECH says to compute rbhs_avail but then doesn't use it
1380 # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1381 # partial_ok, detect_failed)
1383 lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1384 partial_ok, False)
1385 if lbh.is_ro():
1386 lbhs_all.append(lbh)
1388 debug.DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all),
1389 [x.dsa_dnstr for x in lbhs_all]))
1391 # MS-TECH says to compute lbhs_avail but then doesn't use it
1392 # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1393 # partial_ok, detect_failed)
1395 # FOR each nTDSConnection object cn such that the parent of cn is
1396 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1397 for ldsa in lbhs_all:
1398 for cn in ldsa.connect_table.values():
1400 rdsa = rbh_table.get(cn.from_dnstr)
1401 if rdsa is None:
1402 continue
1404 debug.DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1405 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1406 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1407 # cn!transportType references t
1408 if ((cn.is_generated() and
1409 not cn.is_rodc_topology() and
1410 cn.transport_guid == transport.guid)):
1412 # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1413 # cn!options and cn!schedule != sch
1414 # Perform an originating update to set cn!schedule to
1415 # sched
1416 if ((not cn.is_user_owned_schedule() and
1417 not cn.is_equivalent_schedule(link_sched))):
1418 cn.schedule = link_sched
1419 cn.set_modified(True)
1421 # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1422 # NTDSCONN_OPT_USE_NOTIFY are set in cn
1423 if cn.is_override_notify_default() and \
1424 cn.is_use_notify():
1426 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1427 # ri.Options
1428 # Perform an originating update to clear bits
1429 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1430 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1431 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1432 cn.options &= \
1433 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1434 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1435 cn.set_modified(True)
1437 # ELSE
1438 else:
1440 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1441 # ri.Options
1442 # Perform an originating update to set bits
1443 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1444 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1445 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1446 cn.options |= \
1447 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1448 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1449 cn.set_modified(True)
1451 # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1452 if cn.is_twoway_sync():
1454 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1455 # ri.Options
1456 # Perform an originating update to clear bit
1457 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1458 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1459 cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1460 cn.set_modified(True)
1462 # ELSE
1463 else:
1465 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1466 # ri.Options
1467 # Perform an originating update to set bit
1468 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1469 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1470 cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1471 cn.set_modified(True)
1473 # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1474 # in cn!options
1475 if cn.is_intersite_compression_disabled():
1477 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1478 # in ri.Options
1479 # Perform an originating update to clear bit
1480 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1481 # cn!options
1482 if ((link_opt &
1483 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0):
1484 cn.options &= \
1485 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1486 cn.set_modified(True)
1488 # ELSE
1489 else:
1490 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1491 # ri.Options
1492 # Perform an originating update to set bit
1493 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1494 # cn!options
1495 if ((link_opt &
1496 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1497 cn.options |= \
1498 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1499 cn.set_modified(True)
1501 # Display any modified connection
1502 if self.readonly or ldsa.is_ro():
1503 if cn.to_be_modified:
1504 logger.info("TO BE MODIFIED:\n%s" % cn)
1506 ldsa.commit_connections(self.samdb, ro=True)
1507 else:
1508 ldsa.commit_connections(self.samdb)
1509 # ENDFOR
1511 valid_connections = 0
1513 # FOR each nTDSConnection object cn such that cn!parent is
1514 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1515 for ldsa in lbhs_all:
1516 for cn in ldsa.connect_table.values():
1518 rdsa = rbh_table.get(cn.from_dnstr)
1519 if rdsa is None:
1520 continue
1522 debug.DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1524 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1525 # cn!transportType references t) and
1526 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1527 if (((not cn.is_generated() or
1528 cn.transport_guid == transport.guid) and
1529 not cn.is_rodc_topology())):
1531 # LET rguid be the objectGUID of the nTDSDSA object
1532 # referenced by cn!fromServer
1533 # LET lguid be (cn!parent)!objectGUID
1535 # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1536 # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1537 # Increment cValidConnections by 1
1538 if ((not self.is_bridgehead_failed(rdsa, detect_failed) and
1539 not self.is_bridgehead_failed(ldsa, detect_failed))):
1540 valid_connections += 1
1542 # IF keepConnections does not contain cn!objectGUID
1543 # APPEND cn!objectGUID to keepConnections
1544 self.kept_connections.add(cn)
1546 # ENDFOR
1547 debug.DEBUG_RED("valid connections %d" % valid_connections)
1548 DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1549 # IF cValidConnections = 0
1550 if valid_connections == 0:
1552 # LET opt be NTDSCONN_OPT_IS_GENERATED
1553 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1555 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1556 # SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1557 # NTDSCONN_OPT_USE_NOTIFY in opt
1558 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1559 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1560 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1562 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1563 # SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1564 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1565 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1567 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1568 # ri.Options
1569 # SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1570 if ((link_opt &
1571 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1572 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1574 # Perform an originating update to create a new nTDSConnection
1575 # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1576 # cn!options = opt, cn!transportType is a reference to t,
1577 # cn!fromServer is a reference to rbh, and cn!schedule = sch
1578 DEBUG_FN("new connection, KCC dsa: %s" % self.my_dsa.dsa_dnstr)
1579 system_flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1580 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1582 cn = lbh.new_connection(opt, system_flags, transport,
1583 rbh.dsa_dnstr, link_sched)
1585 # Display any added connection
1586 if self.readonly or lbh.is_ro():
1587 if cn.to_be_added:
1588 logger.info("TO BE ADDED:\n%s" % cn)
1590 lbh.commit_connections(self.samdb, ro=True)
1591 else:
1592 lbh.commit_connections(self.samdb)
1594 # APPEND cn!objectGUID to keepConnections
1595 self.kept_connections.add(cn)
1597 def add_transports(self, vertex, local_vertex, graph, detect_failed):
1598 """Build a Vertex's transport lists
1600 Each vertex has accept_red_red and accept_black lists that
1601 list what transports they accept under various conditions. The
1602 only transport that is ever accepted is IP, and a dummy extra
1603 transport called "EDGE_TYPE_ALL".
1605 Part of MS-ADTS 6.2.2.3.4.3 -- ColorVertices
1607 :param vertex: the remote vertex we are thinking about
1608 :param local_vertex: the vertex relating to the local site.
1609 :param graph: the intersite graph
1610 :param detect_failed: whether to detect failed links
1611 :return: True if some bridgeheads were not found
1613 # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1614 # here, but using vertex seems to make more sense. That is,
1615 # the docs want this:
1617 # bh = self.get_bridgehead(local_vertex.site, vertex.part, transport,
1618 # local_vertex.is_black(), detect_failed)
1620 # TODO WHY?????
1622 vertex.accept_red_red = []
1623 vertex.accept_black = []
1624 found_failed = False
1626 if vertex in graph.connected_vertices:
1627 t_guid = str(self.ip_transport.guid)
1629 bh = self.get_bridgehead(vertex.site, vertex.part,
1630 self.ip_transport,
1631 vertex.is_black(), detect_failed)
1632 if bh is None:
1633 if vertex.site.is_rodc_site():
1634 vertex.accept_red_red.append(t_guid)
1635 else:
1636 found_failed = True
1637 else:
1638 vertex.accept_red_red.append(t_guid)
1639 vertex.accept_black.append(t_guid)
1641 # Add additional transport to ensure another run of Dijkstra
1642 vertex.accept_red_red.append("EDGE_TYPE_ALL")
1643 vertex.accept_black.append("EDGE_TYPE_ALL")
1645 return found_failed
1647 def create_connections(self, graph, part, detect_failed):
1648 """Create intersite NTDSConnections as needed by a partition
1650 Construct an NC replica graph for the NC identified by
1651 the given crossRef, then create any additional nTDSConnection
1652 objects required.
1654 :param graph: site graph.
1655 :param part: crossRef object for NC.
1656 :param detect_failed: True to detect failed DCs and route
1657 replication traffic around them, False to assume no DC
1658 has failed.
1660 Modifies self.kept_connections by adding any connections
1661 deemed to be "in use".
1663 :return: (all_connected, found_failed_dc)
1664 (all_connected) True if the resulting NC replica graph
1665 connects all sites that need to be connected.
1666 (found_failed_dc) True if one or more failed DCs were
1667 detected.
1669 all_connected = True
1670 found_failed = False
1672 DEBUG_FN("create_connections(): enter\n"
1673 "\tpartdn=%s\n\tdetect_failed=%s" %
1674 (part.nc_dnstr, detect_failed))
1676 # XXX - This is a highly abbreviated function from the MS-TECH
1677 # ref. It creates connections between bridgeheads to all
1678 # sites that have appropriate replicas. Thus we are not
1679 # creating a minimum cost spanning tree but instead
1680 # producing a fully connected tree. This should produce
1681 # a full (albeit not optimal cost) replication topology.
1683 my_vertex = Vertex(self.my_site, part)
1684 my_vertex.color_vertex()
1686 for v in graph.vertices:
1687 v.color_vertex()
1688 if self.add_transports(v, my_vertex, graph, detect_failed):
1689 found_failed = True
1691 # No NC replicas for this NC in the site of the local DC,
1692 # so no nTDSConnection objects need be created
1693 if my_vertex.is_white():
1694 return all_connected, found_failed
1696 edge_list, n_components = get_spanning_tree_edges(graph,
1697 self.my_site,
1698 label=part.partstr)
1700 DEBUG_FN("%s Number of components: %d" %
1701 (part.nc_dnstr, n_components))
1702 if n_components > 1:
1703 all_connected = False
1705 # LET partialReplicaOkay be TRUE if and only if
1706 # localSiteVertex.Color = COLOR.BLACK
1707 partial_ok = my_vertex.is_black()
1709 # Utilize the IP transport only for now
1710 transport = self.ip_transport
1712 DEBUG("edge_list %s" % edge_list)
1713 for e in edge_list:
1714 # XXX more accurate comparison?
1715 if e.directed and e.vertices[0].site is self.my_site:
1716 continue
1718 if e.vertices[0].site is self.my_site:
1719 rsite = e.vertices[1].site
1720 else:
1721 rsite = e.vertices[0].site
1723 # We don't make connections to our own site as that
1724 # is intrasite topology generator's job
1725 if rsite is self.my_site:
1726 DEBUG("rsite is my_site")
1727 continue
1729 # Determine bridgehead server in remote site
1730 rbh = self.get_bridgehead(rsite, part, transport,
1731 partial_ok, detect_failed)
1732 if rbh is None:
1733 continue
1735 # RODC acts as an BH for itself
1736 # IF AmIRODC() then
1737 # LET lbh be the nTDSDSA object of the local DC
1738 # ELSE
1739 # LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1740 # cr, t, partialReplicaOkay, detectFailedDCs)
1741 if self.my_dsa.is_ro():
1742 lsite = self.my_site
1743 lbh = self.my_dsa
1744 else:
1745 lsite = self.my_site
1746 lbh = self.get_bridgehead(lsite, part, transport,
1747 partial_ok, detect_failed)
1748 # TODO
1749 if lbh is None:
1750 debug.DEBUG_RED("DISASTER! lbh is None")
1751 return False, True
1753 DEBUG_FN("lsite: %s\nrsite: %s" % (lsite, rsite))
1754 DEBUG_FN("vertices %s" % (e.vertices,))
1755 debug.DEBUG_BLUE("bridgeheads\n%s\n%s\n%s" % (lbh, rbh, "-" * 70))
1757 sitelink = e.site_link
1758 if sitelink is None:
1759 link_opt = 0x0
1760 link_sched = None
1761 else:
1762 link_opt = sitelink.options
1763 link_sched = sitelink.schedule
1765 self.create_connection(part, rbh, rsite, transport,
1766 lbh, lsite, link_opt, link_sched,
1767 partial_ok, detect_failed)
1769 return all_connected, found_failed
1771 def create_intersite_connections(self):
1772 """Create NTDSConnections as necessary for all partitions.
1774 Computes an NC replica graph for each NC replica that "should be
1775 present" on the local DC or "is present" on any DC in the same site
1776 as the local DC. For each edge directed to an NC replica on such a
1777 DC from an NC replica on a DC in another site, the KCC creates an
1778 nTDSConnection object to imply that edge if one does not already
1779 exist.
1781 Modifies self.kept_connections - A set of nTDSConnection
1782 objects for edges that are directed
1783 to the local DC's site in one or more NC replica graphs.
1785 :return: True if spanning trees were created for all NC replica
1786 graphs, otherwise False.
1788 all_connected = True
1789 self.kept_connections = set()
1791 # LET crossRefList be the set containing each object o of class
1792 # crossRef such that o is a child of the CN=Partitions child of the
1793 # config NC
1795 # FOR each crossRef object cr in crossRefList
1796 # IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1797 # is clear in cr!systemFlags, skip cr.
1798 # LET g be the GRAPH return of SetupGraph()
1800 for part in self.part_table.values():
1802 if not part.is_enabled():
1803 continue
1805 if part.is_foreign():
1806 continue
1808 graph = self.setup_graph(part)
1810 # Create nTDSConnection objects, routing replication traffic
1811 # around "failed" DCs.
1812 found_failed = False
1814 connected, found_failed = self.create_connections(graph,
1815 part, True)
1817 DEBUG("with detect_failed: connected %s Found failed %s" %
1818 (connected, found_failed))
1819 if not connected:
1820 all_connected = False
1822 if found_failed:
1823 # One or more failed DCs preclude use of the ideal NC
1824 # replica graph. Add connections for the ideal graph.
1825 self.create_connections(graph, part, False)
1827 return all_connected
1829 def intersite(self, ping):
1830 """Generate the inter-site KCC replica graph and nTDSConnections
1832 As per MS-ADTS 6.2.2.3.
1834 If self.readonly is False, the connections are added to self.samdb.
1836 Produces self.kept_connections which is a set of NTDS
1837 Connections that should be kept during subsequent pruning
1838 process.
1840 After this has run, all sites should be connected in a minimum
1841 spanning tree.
1843 :param ping: An oracle function of remote site availability
1844 :return (True or False): (True) if the produced NC replica
1845 graph connects all sites that need to be connected
1848 # Retrieve my DSA
1849 mydsa = self.my_dsa
1850 mysite = self.my_site
1851 all_connected = True
1853 DEBUG_FN("intersite(): enter")
1855 # Determine who is the ISTG
1856 if self.readonly:
1857 mysite.select_istg(self.samdb, mydsa, ro=True)
1858 else:
1859 mysite.select_istg(self.samdb, mydsa, ro=False)
1861 # Test whether local site has topology disabled
1862 if mysite.is_intersite_topology_disabled():
1863 DEBUG_FN("intersite(): exit disabled all_connected=%d" %
1864 all_connected)
1865 return all_connected
1867 if not mydsa.is_istg():
1868 DEBUG_FN("intersite(): exit not istg all_connected=%d" %
1869 all_connected)
1870 return all_connected
1872 self.merge_failed_links(ping)
1874 # For each NC with an NC replica that "should be present" on the
1875 # local DC or "is present" on any DC in the same site as the
1876 # local DC, the KCC constructs a site graph--a precursor to an NC
1877 # replica graph. The site connectivity for a site graph is defined
1878 # by objects of class interSiteTransport, siteLink, and
1879 # siteLinkBridge in the config NC.
1881 all_connected = self.create_intersite_connections()
1883 DEBUG_FN("intersite(): exit all_connected=%d" % all_connected)
1884 return all_connected
1886 # This function currently does no actions. The reason being that we cannot
1887 # perform modifies in this way on the RODC.
1888 def update_rodc_connection(self, ro=True):
1889 """Updates the RODC NTFRS connection object.
1891 If the local DSA is not an RODC, this does nothing.
1893 if not self.my_dsa.is_ro():
1894 return
1896 # Given an nTDSConnection object cn1, such that cn1.options contains
1897 # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1898 # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1899 # that the following is true:
1901 # cn1.fromServer = cn2.fromServer
1902 # cn1.schedule = cn2.schedule
1904 # If no such cn2 can be found, cn1 is not modified.
1905 # If no such cn1 can be found, nothing is modified by this task.
1907 all_connections = self.my_dsa.connect_table.values()
1908 ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1909 rw_connections = [x for x in all_connections
1910 if x not in ro_connections]
1912 # XXX here we are dealing with multiple RODC_TOPO connections,
1913 # if they exist. It is not clear whether the spec means that
1914 # or if it ever arises.
1915 if rw_connections and ro_connections:
1916 for con in ro_connections:
1917 cn2 = rw_connections[0]
1918 con.from_dnstr = cn2.from_dnstr
1919 con.schedule = cn2.schedule
1920 con.to_be_modified = True
1922 self.my_dsa.commit_connections(self.samdb, ro=ro)
1924 def intrasite_max_node_edges(self, node_count):
1925 """Find the maximum number of edges directed to an intrasite node
1927 The KCC does not create more than 50 edges directed to a
1928 single DC. To optimize replication, we compute that each node
1929 should have n+2 total edges directed to it such that (n) is
1930 the smallest non-negative integer satisfying
1931 (node_count <= 2*(n*n) + 6*n + 7)
1933 (If the number of edges is m (i.e. n + 2), that is the same as
1934 2 * m*m - 2 * m + 3). We think in terms of n because that is
1935 the number of extra connections over the double directed ring
1936 that exists by default.
1938 edges n nodecount
1939 2 0 7
1940 3 1 15
1941 4 2 27
1942 5 3 43
1944 50 48 4903
1946 :param node_count: total number of nodes in the replica graph
1948 The intention is that there should be no more than 3 hops
1949 between any two DSAs at a site. With up to 7 nodes the 2 edges
1950 of the ring are enough; any configuration of extra edges with
1951 8 nodes will be enough. It is less clear that the 3 hop
1952 guarantee holds at e.g. 15 nodes in degenerate cases, but
1953 those are quite unlikely given the extra edges are randomly
1954 arranged.
1956 :param node_count: the number of nodes in the site
1957 "return: The desired maximum number of connections
1959 n = 0
1960 while True:
1961 if node_count <= (2 * (n * n) + (6 * n) + 7):
1962 break
1963 n = n + 1
1964 n = n + 2
1965 if n < 50:
1966 return n
1967 return 50
1969 def construct_intrasite_graph(self, site_local, dc_local,
1970 nc_x, gc_only, detect_stale):
1971 """Create an intrasite graph using given parameters
1973 This might be called a number of times per site with different
1974 parameters.
1976 Based on [MS-ADTS] 6.2.2.2
1978 :param site_local: site for which we are working
1979 :param dc_local: local DC that potentially needs a replica
1980 :param nc_x: naming context (x) that we are testing if it
1981 "should be present" on the local DC
1982 :param gc_only: Boolean - only consider global catalog servers
1983 :param detect_stale: Boolean - check whether links seems down
1984 :return: None
1986 # We're using the MS notation names here to allow
1987 # correlation back to the published algorithm.
1989 # nc_x - naming context (x) that we are testing if it
1990 # "should be present" on the local DC
1991 # f_of_x - replica (f) found on a DC (s) for NC (x)
1992 # dc_s - DC where f_of_x replica was found
1993 # dc_local - local DC that potentially needs a replica
1994 # (f_of_x)
1995 # r_list - replica list R
1996 # p_of_x - replica (p) is partial and found on a DC (s)
1997 # for NC (x)
1998 # l_of_x - replica (l) is the local replica for NC (x)
1999 # that should appear on the local DC
2000 # r_len = is length of replica list |R|
2002 # If the DSA doesn't need a replica for this
2003 # partition (NC x) then continue
2004 needed, ro, partial = nc_x.should_be_present(dc_local)
2006 debug.DEBUG_YELLOW("construct_intrasite_graph(): enter" +
2007 "\n\tgc_only=%d" % gc_only +
2008 "\n\tdetect_stale=%d" % detect_stale +
2009 "\n\tneeded=%s" % needed +
2010 "\n\tro=%s" % ro +
2011 "\n\tpartial=%s" % partial +
2012 "\n%s" % nc_x)
2014 if not needed:
2015 debug.DEBUG_RED("%s lacks 'should be present' status, "
2016 "aborting construct_intrasite_graph!" %
2017 nc_x.nc_dnstr)
2018 return
2020 # Create a NCReplica that matches what the local replica
2021 # should say. We'll use this below in our r_list
2022 l_of_x = NCReplica(dc_local, nc_x.nc_dnstr)
2024 l_of_x.identify_by_basedn(self.samdb)
2026 l_of_x.rep_partial = partial
2027 l_of_x.rep_ro = ro
2029 # Add this replica that "should be present" to the
2030 # needed replica table for this DSA
2031 dc_local.add_needed_replica(l_of_x)
2033 # Replica list
2035 # Let R be a sequence containing each writable replica f of x
2036 # such that f "is present" on a DC s satisfying the following
2037 # criteria:
2039 # * s is a writable DC other than the local DC.
2041 # * s is in the same site as the local DC.
2043 # * If x is a read-only full replica and x is a domain NC,
2044 # then the DC's functional level is at least
2045 # DS_BEHAVIOR_WIN2008.
2047 # * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
2048 # in the options attribute of the site settings object for
2049 # the local DC's site, or no tuple z exists in the
2050 # kCCFailedLinks or kCCFailedConnections variables such
2051 # that z.UUIDDsa is the objectGUID of the nTDSDSA object
2052 # for s, z.FailureCount > 0, and the current time -
2053 # z.TimeFirstFailure > 2 hours.
2055 r_list = []
2057 # We'll loop thru all the DSAs looking for
2058 # writeable NC replicas that match the naming
2059 # context dn for (nc_x)
2061 for dc_s in self.my_site.dsa_table.values():
2062 # If this partition (nc_x) doesn't appear as a
2063 # replica (f_of_x) on (dc_s) then continue
2064 if nc_x.nc_dnstr not in dc_s.current_rep_table:
2065 continue
2067 # Pull out the NCReplica (f) of (x) with the dn
2068 # that matches NC (x) we are examining.
2069 f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2071 # Replica (f) of NC (x) must be writable
2072 if f_of_x.is_ro():
2073 continue
2075 # Replica (f) of NC (x) must satisfy the
2076 # "is present" criteria for DC (s) that
2077 # it was found on
2078 if not f_of_x.is_present():
2079 continue
2081 # DC (s) must be a writable DSA other than
2082 # my local DC. In other words we'd only replicate
2083 # from other writable DC
2084 if dc_s.is_ro() or dc_s is dc_local:
2085 continue
2087 # Certain replica graphs are produced only
2088 # for global catalogs, so test against
2089 # method input parameter
2090 if gc_only and not dc_s.is_gc():
2091 continue
2093 # DC (s) must be in the same site as the local DC
2094 # as this is the intra-site algorithm. This is
2095 # handled by virtue of placing DSAs in per
2096 # site objects (see enclosing for() loop)
2098 # If NC (x) is intended to be read-only full replica
2099 # for a domain NC on the target DC then the source
2100 # DC should have functional level at minimum WIN2008
2102 # Effectively we're saying that in order to replicate
2103 # to a targeted RODC (which was introduced in Windows 2008)
2104 # then we have to replicate from a DC that is also minimally
2105 # at that level.
2107 # You can also see this requirement in the MS special
2108 # considerations for RODC which state that to deploy
2109 # an RODC, at least one writable domain controller in
2110 # the domain must be running Windows Server 2008
2111 if ro and not partial and nc_x.nc_type == NCType.domain:
2112 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2113 continue
2115 # If we haven't been told to turn off stale connection
2116 # detection and this dsa has a stale connection then
2117 # continue
2118 if detect_stale and self.is_stale_link_connection(dc_s):
2119 continue
2121 # Replica meets criteria. Add it to table indexed
2122 # by the GUID of the DC that it appears on
2123 r_list.append(f_of_x)
2125 # If a partial (not full) replica of NC (x) "should be present"
2126 # on the local DC, append to R each partial replica (p of x)
2127 # such that p "is present" on a DC satisfying the same
2128 # criteria defined above for full replica DCs.
2130 # XXX This loop and the previous one differ only in whether
2131 # the replica is partial or not. here we only accept partial
2132 # (because we're partial); before we only accepted full. Order
2133 # doesn't matter (the list is sorted a few lines down) so these
2134 # loops could easily be merged. Or this could be a helper
2135 # function.
2137 if partial:
2138 # Now we loop thru all the DSAs looking for
2139 # partial NC replicas that match the naming
2140 # context dn for (NC x)
2141 for dc_s in self.my_site.dsa_table.values():
2143 # If this partition NC (x) doesn't appear as a
2144 # replica (p) of NC (x) on the dsa DC (s) then
2145 # continue
2146 if nc_x.nc_dnstr not in dc_s.current_rep_table:
2147 continue
2149 # Pull out the NCReplica with the dn that
2150 # matches NC (x) we are examining.
2151 p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2153 # Replica (p) of NC (x) must be partial
2154 if not p_of_x.is_partial():
2155 continue
2157 # Replica (p) of NC (x) must satisfy the
2158 # "is present" criteria for DC (s) that
2159 # it was found on
2160 if not p_of_x.is_present():
2161 continue
2163 # DC (s) must be a writable DSA other than
2164 # my DSA. In other words we'd only replicate
2165 # from other writable DSA
2166 if dc_s.is_ro() or dc_s is dc_local:
2167 continue
2169 # Certain replica graphs are produced only
2170 # for global catalogs, so test against
2171 # method input parameter
2172 if gc_only and not dc_s.is_gc():
2173 continue
2175 # If we haven't been told to turn off stale connection
2176 # detection and this dsa has a stale connection then
2177 # continue
2178 if detect_stale and self.is_stale_link_connection(dc_s):
2179 continue
2181 # Replica meets criteria. Add it to table indexed
2182 # by the GUID of the DSA that it appears on
2183 r_list.append(p_of_x)
2185 # Append to R the NC replica that "should be present"
2186 # on the local DC
2187 r_list.append(l_of_x)
2189 r_list.sort(key=lambda rep: ndr_pack(rep.rep_dsa_guid))
2190 r_len = len(r_list)
2192 max_node_edges = self.intrasite_max_node_edges(r_len)
2194 # Add a node for each r_list element to the replica graph
2195 graph_list = []
2196 for rep in r_list:
2197 node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2198 graph_list.append(node)
2200 # For each r(i) from (0 <= i < |R|-1)
2201 i = 0
2202 while i < (r_len - 1):
2203 # Add an edge from r(i) to r(i+1) if r(i) is a full
2204 # replica or r(i+1) is a partial replica
2205 if not r_list[i].is_partial() or r_list[i +1].is_partial():
2206 graph_list[i + 1].add_edge_from(r_list[i].rep_dsa_dnstr)
2208 # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2209 # replica or ri is a partial replica.
2210 if not r_list[i + 1].is_partial() or r_list[i].is_partial():
2211 graph_list[i].add_edge_from(r_list[i + 1].rep_dsa_dnstr)
2212 i = i + 1
2214 # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2215 # or r0 is a partial replica.
2216 if not r_list[r_len - 1].is_partial() or r_list[0].is_partial():
2217 graph_list[0].add_edge_from(r_list[r_len - 1].rep_dsa_dnstr)
2219 # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2220 # r|R|-1 is a partial replica.
2221 if not r_list[0].is_partial() or r_list[r_len -1].is_partial():
2222 graph_list[r_len - 1].add_edge_from(r_list[0].rep_dsa_dnstr)
2224 DEBUG("r_list is length %s" % len(r_list))
2225 DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr))
2226 for x in r_list))
2228 do_dot_files = self.dot_file_dir is not None and self.debug
2229 if self.verify or do_dot_files:
2230 dot_edges = []
2231 dot_vertices = set()
2232 for v1 in graph_list:
2233 dot_vertices.add(v1.dsa_dnstr)
2234 for v2 in v1.edge_from:
2235 dot_edges.append((v2, v1.dsa_dnstr))
2236 dot_vertices.add(v2)
2238 verify_properties = ('connected',)
2239 verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2240 label='%s__%s__%s' % (site_local.site_dnstr,
2241 nctype_lut[nc_x.nc_type],
2242 nc_x.nc_dnstr),
2243 properties=verify_properties, debug=DEBUG,
2244 verify=self.verify,
2245 dot_file_dir=self.dot_file_dir,
2246 directed=True)
2248 rw_dot_vertices = set(x for x in dot_vertices
2249 if not self.get_dsa(x).is_ro())
2250 rw_dot_edges = [(a, b) for a, b in dot_edges if
2251 a in rw_dot_vertices and b in rw_dot_vertices]
2252 rw_verify_properties = ('connected',
2253 'directed_double_ring_or_small')
2254 verify_and_dot('intrasite_rw_pre_ntdscon', rw_dot_edges,
2255 rw_dot_vertices,
2256 label='%s__%s__%s' % (site_local.site_dnstr,
2257 nctype_lut[nc_x.nc_type],
2258 nc_x.nc_dnstr),
2259 properties=rw_verify_properties, debug=DEBUG,
2260 verify=self.verify,
2261 dot_file_dir=self.dot_file_dir,
2262 directed=True)
2264 # For each existing nTDSConnection object implying an edge
2265 # from rj of R to ri such that j != i, an edge from rj to ri
2266 # is not already in the graph, and the total edges directed
2267 # to ri is less than n+2, the KCC adds that edge to the graph.
2268 for vertex in graph_list:
2269 dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2270 for connect in dsa.connect_table.values():
2271 remote = connect.from_dnstr
2272 if remote in self.my_site.dsa_table:
2273 vertex.add_edge_from(remote)
2275 DEBUG('reps are: %s' % ' '.join(x.rep_dsa_dnstr for x in r_list))
2276 DEBUG('dsas are: %s' % ' '.join(x.dsa_dnstr for x in graph_list))
2278 for tnode in graph_list:
2279 # To optimize replication latency in sites with many NC
2280 # replicas, the KCC adds new edges directed to ri to bring
2281 # the total edges to n+2, where the NC replica rk of R
2282 # from which the edge is directed is chosen at random such
2283 # that k != i and an edge from rk to ri is not already in
2284 # the graph.
2286 # Note that the KCC tech ref does not give a number for
2287 # the definition of "sites with many NC replicas". At a
2288 # bare minimum to satisfy n+2 edges directed at a node we
2289 # have to have at least three replicas in |R| (i.e. if n
2290 # is zero then at least replicas from two other graph
2291 # nodes may direct edges to us).
2292 if r_len >= 3 and not tnode.has_sufficient_edges():
2293 candidates = [x for x in graph_list if
2294 (x is not tnode and
2295 x.dsa_dnstr not in tnode.edge_from)]
2297 debug.DEBUG_BLUE("looking for random link for %s. r_len %d, "
2298 "graph len %d candidates %d"
2299 % (tnode.dsa_dnstr, r_len, len(graph_list),
2300 len(candidates)))
2302 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2304 while candidates and not tnode.has_sufficient_edges():
2305 other = random.choice(candidates)
2306 DEBUG("trying to add candidate %s" % other.dsa_dnstr)
2307 if not tnode.add_edge_from(other.dsa_dnstr):
2308 debug.DEBUG_RED("could not add %s" % other.dsa_dnstr)
2309 candidates.remove(other)
2310 else:
2311 DEBUG_FN("not adding links to %s: nodes %s, links is %s/%s" %
2312 (tnode.dsa_dnstr, r_len, len(tnode.edge_from),
2313 tnode.max_edges))
2315 # Print the graph node in debug mode
2316 DEBUG_FN("%s" % tnode)
2318 # For each edge directed to the local DC, ensure a nTDSConnection
2319 # points to us that satisfies the KCC criteria
2321 if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2322 tnode.add_connections_from_edges(dc_local, self.ip_transport)
2324 if self.verify or do_dot_files:
2325 dot_edges = []
2326 dot_vertices = set()
2327 for v1 in graph_list:
2328 dot_vertices.add(v1.dsa_dnstr)
2329 for v2 in v1.edge_from:
2330 dot_edges.append((v2, v1.dsa_dnstr))
2331 dot_vertices.add(v2)
2333 verify_properties = ('connected',)
2334 verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2335 label='%s__%s__%s' % (site_local.site_dnstr,
2336 nctype_lut[nc_x.nc_type],
2337 nc_x.nc_dnstr),
2338 properties=verify_properties, debug=DEBUG,
2339 verify=self.verify,
2340 dot_file_dir=self.dot_file_dir,
2341 directed=True)
2343 rw_dot_vertices = set(x for x in dot_vertices
2344 if not self.get_dsa(x).is_ro())
2345 rw_dot_edges = [(a, b) for a, b in dot_edges if
2346 a in rw_dot_vertices and b in rw_dot_vertices]
2347 rw_verify_properties = ('connected',
2348 'directed_double_ring_or_small')
2349 verify_and_dot('intrasite_rw_post_ntdscon', rw_dot_edges,
2350 rw_dot_vertices,
2351 label='%s__%s__%s' % (site_local.site_dnstr,
2352 nctype_lut[nc_x.nc_type],
2353 nc_x.nc_dnstr),
2354 properties=rw_verify_properties, debug=DEBUG,
2355 verify=self.verify,
2356 dot_file_dir=self.dot_file_dir,
2357 directed=True)
2359 def intrasite(self):
2360 """Generate the intrasite KCC connections
2362 As per MS-ADTS 6.2.2.2.
2364 If self.readonly is False, the connections are added to self.samdb.
2366 After this call, all DCs in each site with more than 3 DCs
2367 should be connected in a bidirectional ring. If a site has 2
2368 DCs, they will bidirectionally connected. Sites with many DCs
2369 may have arbitrary extra connections.
2371 :return: None
2373 mydsa = self.my_dsa
2375 DEBUG_FN("intrasite(): enter")
2377 # Test whether local site has topology disabled
2378 mysite = self.my_site
2379 if mysite.is_intrasite_topology_disabled():
2380 return
2382 detect_stale = (not mysite.is_detect_stale_disabled())
2383 for connect in mydsa.connect_table.values():
2384 if connect.to_be_added:
2385 debug.DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2387 # Loop thru all the partitions, with gc_only False
2388 for partdn, part in self.part_table.items():
2389 self.construct_intrasite_graph(mysite, mydsa, part, False,
2390 detect_stale)
2391 for connect in mydsa.connect_table.values():
2392 if connect.to_be_added:
2393 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2395 # If the DC is a GC server, the KCC constructs an additional NC
2396 # replica graph (and creates nTDSConnection objects) for the
2397 # config NC as above, except that only NC replicas that "are present"
2398 # on GC servers are added to R.
2399 for connect in mydsa.connect_table.values():
2400 if connect.to_be_added:
2401 debug.DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2403 # Do it again, with gc_only True
2404 for partdn, part in self.part_table.items():
2405 if part.is_config():
2406 self.construct_intrasite_graph(mysite, mydsa, part, True,
2407 detect_stale)
2409 # The DC repeats the NC replica graph computation and nTDSConnection
2410 # creation for each of the NC replica graphs, this time assuming
2411 # that no DC has failed. It does so by re-executing the steps as
2412 # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2413 # set in the options attribute of the site settings object for
2414 # the local DC's site. (ie. we set "detec_stale" flag to False)
2415 for connect in mydsa.connect_table.values():
2416 if connect.to_be_added:
2417 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2419 # Loop thru all the partitions.
2420 for partdn, part in self.part_table.items():
2421 self.construct_intrasite_graph(mysite, mydsa, part, False,
2422 False) # don't detect stale
2424 # If the DC is a GC server, the KCC constructs an additional NC
2425 # replica graph (and creates nTDSConnection objects) for the
2426 # config NC as above, except that only NC replicas that "are present"
2427 # on GC servers are added to R.
2428 for connect in mydsa.connect_table.values():
2429 if connect.to_be_added:
2430 debug.DEBUG_RED("TO BE ADDED:\n%s" % connect)
2432 for partdn, part in self.part_table.items():
2433 if part.is_config():
2434 self.construct_intrasite_graph(mysite, mydsa, part, True,
2435 False) # don't detect stale
2437 self._commit_changes(mydsa)
2439 def list_dsas(self):
2440 """Compile a comprehensive list of DSA DNs
2442 These are all the DSAs on all the sites that KCC would be
2443 dealing with.
2445 This method is not idempotent and may not work correctly in
2446 sequence with KCC.run().
2448 :return: a list of DSA DN strings.
2450 self.load_my_site()
2451 self.load_my_dsa()
2453 self.load_all_sites()
2454 self.load_all_partitions()
2455 self.load_ip_transport()
2456 self.load_all_sitelinks()
2457 dsas = []
2458 for site in self.site_table.values():
2459 dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2460 for dsa in site.dsa_table.values()])
2461 return dsas
2463 def load_samdb(self, dburl, lp, creds, force=False):
2464 """Load the database using an url, loadparm, and credentials
2466 If force is False, the samdb won't be reloaded if it already
2467 exists.
2469 :param dburl: a database url.
2470 :param lp: a loadparm object.
2471 :param creds: a Credentials object.
2472 :param force: a boolean indicating whether to overwrite.
2475 if force or self.samdb is None:
2476 try:
2477 self.samdb = SamDB(url=dburl,
2478 session_info=system_session(),
2479 credentials=creds, lp=lp)
2480 except ldb.LdbError as e1:
2481 (num, msg) = e1.args
2482 raise KCCError("Unable to open sam database %s : %s" %
2483 (dburl, msg))
2485 def plot_all_connections(self, basename, verify_properties=()):
2486 """Helper function to plot and verify NTDSConnections
2488 :param basename: an identifying string to use in filenames and logs.
2489 :param verify_properties: properties to verify (default empty)
2491 verify = verify_properties and self.verify
2492 if not verify and self.dot_file_dir is None:
2493 return
2495 dot_edges = []
2496 dot_vertices = []
2497 edge_colours = []
2498 vertex_colours = []
2500 for dsa in self.dsa_by_dnstr.values():
2501 dot_vertices.append(dsa.dsa_dnstr)
2502 if dsa.is_ro():
2503 vertex_colours.append('#cc0000')
2504 else:
2505 vertex_colours.append('#0000cc')
2506 for con in dsa.connect_table.values():
2507 if con.is_rodc_topology():
2508 edge_colours.append('red')
2509 else:
2510 edge_colours.append('blue')
2511 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2513 verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2514 label=self.my_dsa_dnstr,
2515 properties=verify_properties, debug=DEBUG,
2516 verify=verify, dot_file_dir=self.dot_file_dir,
2517 directed=True, edge_colors=edge_colours,
2518 vertex_colors=vertex_colours)
2520 def run(self, dburl, lp, creds, forced_local_dsa=None,
2521 forget_local_links=False, forget_intersite_links=False,
2522 attempt_live_connections=False):
2523 """Perform a KCC run, possibly updating repsFrom topology
2525 :param dburl: url of the database to work with.
2526 :param lp: a loadparm object.
2527 :param creds: a Credentials object.
2528 :param forced_local_dsa: pretend to be on the DSA with this dn_str
2529 :param forget_local_links: calculate as if no connections existed
2530 (boolean, default False)
2531 :param forget_intersite_links: calculate with only intrasite connection
2532 (boolean, default False)
2533 :param attempt_live_connections: attempt to connect to remote DSAs to
2534 determine link availability (boolean, default False)
2535 :return: 1 on error, 0 otherwise
2537 if self.samdb is None:
2538 DEBUG_FN("samdb is None; let's load it from %s" % (dburl,))
2539 self.load_samdb(dburl, lp, creds, force=False)
2541 if forced_local_dsa:
2542 self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" %
2543 forced_local_dsa)
2545 try:
2546 # Setup
2547 self.load_my_site()
2548 self.load_my_dsa()
2550 self.load_all_sites()
2551 self.load_all_partitions()
2552 self.load_ip_transport()
2553 self.load_all_sitelinks()
2555 if self.verify or self.dot_file_dir is not None:
2556 guid_to_dnstr = {}
2557 for site in self.site_table.values():
2558 guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2559 for dnstr, dsa
2560 in site.dsa_table.items())
2562 self.plot_all_connections('dsa_initial')
2564 dot_edges = []
2565 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2566 for dnstr, c_rep in current_reps.items():
2567 DEBUG("c_rep %s" % c_rep)
2568 dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2570 verify_and_dot('dsa_repsFrom_initial', dot_edges,
2571 directed=True, label=self.my_dsa_dnstr,
2572 properties=(), debug=DEBUG, verify=self.verify,
2573 dot_file_dir=self.dot_file_dir)
2575 dot_edges = []
2576 for site in self.site_table.values():
2577 for dsa in site.dsa_table.values():
2578 current_reps, needed_reps = dsa.get_rep_tables()
2579 for dn_str, rep in current_reps.items():
2580 for reps_from in rep.rep_repsFrom:
2581 DEBUG("rep %s" % rep)
2582 dsa_guid = str(reps_from.source_dsa_obj_guid)
2583 dsa_dn = guid_to_dnstr[dsa_guid]
2584 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2586 verify_and_dot('dsa_repsFrom_initial_all', dot_edges,
2587 directed=True, label=self.my_dsa_dnstr,
2588 properties=(), debug=DEBUG, verify=self.verify,
2589 dot_file_dir=self.dot_file_dir)
2591 dot_edges = []
2592 dot_colours = []
2593 for link in self.sitelink_table.values():
2594 from hashlib import md5
2595 tmp_str = link.dnstr.encode('utf8')
2596 colour = '#' + md5(tmp_str).hexdigest()[:6]
2597 for a, b in itertools.combinations(link.site_list, 2):
2598 dot_edges.append((a[1], b[1]))
2599 dot_colours.append(colour)
2600 properties = ('connected',)
2601 verify_and_dot('dsa_sitelink_initial', dot_edges,
2602 directed=False,
2603 label=self.my_dsa_dnstr, properties=properties,
2604 debug=DEBUG, verify=self.verify,
2605 dot_file_dir=self.dot_file_dir,
2606 edge_colors=dot_colours)
2608 if forget_local_links:
2609 for dsa in self.my_site.dsa_table.values():
2610 dsa.connect_table = dict((k, v) for k, v in
2611 dsa.connect_table.items()
2612 if v.is_rodc_topology() or
2613 (v.from_dnstr not in
2614 self.my_site.dsa_table))
2615 self.plot_all_connections('dsa_forgotten_local')
2617 if forget_intersite_links:
2618 for site in self.site_table.values():
2619 for dsa in site.dsa_table.values():
2620 dsa.connect_table = dict((k, v) for k, v in
2621 dsa.connect_table.items()
2622 if site is self.my_site and
2623 v.is_rodc_topology())
2625 self.plot_all_connections('dsa_forgotten_all')
2627 if attempt_live_connections:
2628 # Encapsulates lp and creds in a function that
2629 # attempts connections to remote DSAs.
2630 def ping(self, dnsname):
2631 try:
2632 drs_utils.drsuapi_connect(dnsname, self.lp, self.creds)
2633 except drs_utils.drsException:
2634 return False
2635 return True
2636 else:
2637 ping = None
2638 # These are the published steps (in order) for the
2639 # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2641 # Step 1
2642 self.refresh_failed_links_connections(ping)
2644 # Step 2
2645 self.intrasite()
2647 # Step 3
2648 all_connected = self.intersite(ping)
2650 # Step 4
2651 self.remove_unneeded_ntdsconn(all_connected)
2653 # Step 5
2654 self.translate_ntdsconn()
2656 # Step 6
2657 self.remove_unneeded_failed_links_connections()
2659 # Step 7
2660 self.update_rodc_connection()
2662 if self.verify or self.dot_file_dir is not None:
2663 self.plot_all_connections('dsa_final',
2664 ('connected',))
2666 debug.DEBUG_MAGENTA("there are %d dsa guids" %
2667 len(guid_to_dnstr))
2669 dot_edges = []
2670 edge_colors = []
2671 my_dnstr = self.my_dsa.dsa_dnstr
2672 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2673 for dnstr, n_rep in needed_reps.items():
2674 for reps_from in n_rep.rep_repsFrom:
2675 guid_str = str(reps_from.source_dsa_obj_guid)
2676 dot_edges.append((my_dnstr, guid_to_dnstr[guid_str]))
2677 edge_colors.append('#' + str(n_rep.nc_guid)[:6])
2679 verify_and_dot('dsa_repsFrom_final', dot_edges, directed=True,
2680 label=self.my_dsa_dnstr,
2681 properties=(), debug=DEBUG, verify=self.verify,
2682 dot_file_dir=self.dot_file_dir,
2683 edge_colors=edge_colors)
2685 dot_edges = []
2687 for site in self.site_table.values():
2688 for dsa in site.dsa_table.values():
2689 current_reps, needed_reps = dsa.get_rep_tables()
2690 for n_rep in needed_reps.values():
2691 for reps_from in n_rep.rep_repsFrom:
2692 dsa_guid = str(reps_from.source_dsa_obj_guid)
2693 dsa_dn = guid_to_dnstr[dsa_guid]
2694 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2696 verify_and_dot('dsa_repsFrom_final_all', dot_edges,
2697 directed=True, label=self.my_dsa_dnstr,
2698 properties=(), debug=DEBUG, verify=self.verify,
2699 dot_file_dir=self.dot_file_dir)
2701 except:
2702 raise
2704 return 0
2706 def import_ldif(self, dburl, lp, ldif_file, forced_local_dsa=None):
2707 """Import relevant objects and attributes from an LDIF file.
2709 The point of this function is to allow a programmer/debugger to
2710 import an LDIF file with non-security relevant information that
2711 was previously extracted from a DC database. The LDIF file is used
2712 to create a temporary abbreviated database. The KCC algorithm can
2713 then run against this abbreviated database for debug or test
2714 verification that the topology generated is computationally the
2715 same between different OSes and algorithms.
2717 :param dburl: path to the temporary abbreviated db to create
2718 :param lp: a loadparm object.
2719 :param ldif_file: path to the ldif file to import
2720 :param forced_local_dsa: perform KCC from this DSA's point of view
2721 :return: zero on success, 1 on error
2723 try:
2724 self.samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif_file,
2725 forced_local_dsa)
2726 except ldif_import_export.LdifError as e:
2727 logger.critical(e)
2728 return 1
2729 return 0
2731 def export_ldif(self, dburl, lp, creds, ldif_file):
2732 """Save KCC relevant details to an ldif file
2734 The point of this function is to allow a programmer/debugger to
2735 extract an LDIF file with non-security relevant information from
2736 a DC database. The LDIF file can then be used to "import" via
2737 the import_ldif() function this file into a temporary abbreviated
2738 database. The KCC algorithm can then run against this abbreviated
2739 database for debug or test verification that the topology generated
2740 is computationally the same between different OSes and algorithms.
2742 :param dburl: LDAP database URL to extract info from
2743 :param lp: a loadparm object.
2744 :param cred: a Credentials object.
2745 :param ldif_file: output LDIF file name to create
2746 :return: zero on success, 1 on error
2748 try:
2749 ldif_import_export.samdb_to_ldif_file(self.samdb, dburl, lp, creds,
2750 ldif_file)
2751 except ldif_import_export.LdifError as e:
2752 logger.critical(e)
2753 return 1
2754 return 0