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/>.
24 from functools
import cmp_to_key
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
56 :param dsa1: A DSA object
57 :param dsa2: Another DSA
58 :return: -1, 0, or 1, indicating sort order.
60 if dsa1
.is_gc() and not dsa2
.is_gc():
62 if not dsa1
.is_gc() and dsa2
.is_gc():
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)
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
91 def __init__(self
, unix_now
, readonly
=False, verify
=False, debug
=False,
93 """Initializes the partitions class which can hold
94 our local DCs partitions or all the partitions in
97 self
.part_table
= {} # partition objects
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
126 self
.unix_now
= unix_now
127 self
.nt_now
= unix2nttime(unix_now
)
128 self
.readonly
= readonly
131 self
.dot_file_dir
= dot_file_dir
133 def load_ip_transport(self
):
134 """Loads the inter-site transport objects for Sites
137 :raise KCCError: if no IP transport is found
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)" %
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 "
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
172 :raise KCCError: if site-links aren't found
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
)
187 if dnstr
in self
.sitelink_table
:
190 sitelink
= SiteLink(dnstr
)
192 sitelink
.load_sitelink(self
.samdb
)
194 # Assign this siteLink to table
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.
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.
236 :raise: KCCError if sites can't be found
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
)
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
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
)
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
))
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
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
)
287 raise KCCError("Unable to find my nTDSDSA at %s" %
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)" %
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)
316 :raise: KCCError if partitions can't be found
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
)
328 partstr
= str(msg
.dn
)
331 if partstr
in self
.part_table
:
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
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:
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
)
364 f
= KCCFailedObject(dsa_guid
, failure_count
,
365 time_first_failure
, last_result
,
367 self
.kcc_failed_links
[dsa_guid
] = f
369 f
.failure_count
= max(f
.failure_count
, failure_count
)
370 f
.time_first_failure
= min(f
.time_first_failure
,
372 f
.last_result
= last_result
374 # CONNECTIONS: Refresh failed connections
375 restore_connections
= set()
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
)
383 connection
.failure_count
+= 1
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
))
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:
418 # We have checked failed *links*, but we also need to check
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.
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.
442 for cn_conn
in connections
:
443 if cn_conn
.guid
is None:
445 cn_conn
.guid
= misc
.GUID(str(uuid
.uuid4()))
446 cn_conn
.whenCreated
= self
.nt_now
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.
458 for cn_conn
in self
.my_dsa
.connect_table
.values():
459 s_dnstr
= cn_conn
.get_from_dnstr()
461 DEBUG_FN("%s has phantom connection %s" % (self
.my_dsa
,
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.
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
)
485 self
._ensure
_connections
_are
_loaded
(mydsa
.connect_table
.values())
487 # RODC never actually added any connections to begin with
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):
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
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.
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():
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():
538 s_dnstr
= cn
.get_from_dnstr()
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"
547 cn
.to_be_deleted
= True
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():
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))):
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)
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
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
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
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
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:
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
686 if ((t_repsFrom
.replica_flags
&
687 drsuapi
.DRSUAPI_DRS_NEVER_NOTIFY
) == 0x0):
688 t_repsFrom
.replica_flags |
= \
689 drsuapi
.DRSUAPI_DRS_NEVER_NOTIFY
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
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
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.
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
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
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
837 if not cn_conn
.is_enabled() or cn_conn
.is_rodc_topology():
840 s_dnstr
= cn_conn
.get_from_dnstr()
841 s_dsa
= self
.get_dsa(s_dnstr
)
843 # No DSA matching this source DN string?
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())):
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.
868 if current_dsa
is None:
869 current_dsa
= self
.my_dsa
871 if current_dsa
.is_ro():
874 if current_dsa
.is_translate_ntdsconn_disabled():
875 DEBUG_FN("skipping translate_ntdsconn() "
876 "because disabling flag is set")
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
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
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
)
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
)))
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
]
930 # Now perform the scan of replicas we'll need
931 # and compare any current repsFrom against the
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
953 logger
.warning("repsFrom source DSA guid (%s) not found" %
955 t_repsFrom
.to_be_deleted
= True
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():
967 # no break means no non-rodc_topology connection exists
968 t_repsFrom
.to_be_deleted
= True
971 # KCC removes this repsFrom tuple if any of the following
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
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
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
)
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
):
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()
1030 logger
.info("TO BE DELETED:\n%s" % text
)
1031 text
= n_rep
.dumpstr_to_be_modified()
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)
1039 # Commit any modified repsFrom to the NC replica
1040 n_rep
.commit_repsFrom(self
.samdb
)
1044 # Now perform the scan of replicas we'll need
1045 # and compare any current repsTo against the
1048 # RODC should never push to anybody (should we check this?)
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
1071 logger
.warning("repsTo source DSA guid (%s) not found" %
1073 t_repsTo
.to_be_deleted
= True
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" %
1083 t_repsTo
.to_be_deleted
= True
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
1091 # There is no plausible connection for this repsTo
1092 t_repsTo
.to_be_deleted
= True
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)
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
1120 :param ping: An oracle of current bridgehead availability
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")
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
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
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:
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
,
1171 dot_file_dir
=self
.dot_file_dir
)
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
1182 :param part: crossRef for NC to replicate.
1183 :param transport: interSiteTransport object for replication
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
1191 :return: dsa object for the bridgehead DC or None
1194 bhs
= self
.get_all_bridgeheads(site
, part
, transport
,
1195 partial_ok
, detect_failed
)
1197 debug
.DEBUG_MAGENTA("get_bridgehead FAILED:\nsitedn = %s" %
1201 debug
.DEBUG_GREEN("get_bridgehead:\n\tsitedn = %s\n\tbhdn = %s" %
1202 (site
.site_dnstr
, bhs
[0].dsa_dnstr
))
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
1222 :return: list of dsa object for available bridgehead DCs
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
)):
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
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
):
1252 rep
= dsa
.get_current_replica(part
.nc_dnstr
)
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
1260 rep
= dsa
.get_current_replica(part
.nc_dnstr
)
1261 if rep
is None or (rep
.is_partial() and not partial_ok
):
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
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
):
1272 # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1274 if self
.is_bridgehead_failed(dsa
, detect_failed
):
1275 DEBUG("bridgehead is failed")
1278 DEBUG_FN("found a bridgehead: %s" % dsa
.dsa_dnstr
)
1281 # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1283 # SORT bhs such that all GC servers precede DCs that are not GC
1284 # servers, and otherwise by ascending objectGUID
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
))
1291 debug
.DEBUG_YELLOW(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
1309 * RETURNS: TRUE if and only if the DC should be considered to be in a
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
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
1324 RETURN detectFailedDCs
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
:
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:
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,
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
1372 rbhs_all
= self
.get_all_bridgeheads(rsite
, part
, transport
,
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
,
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
)
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
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 \
1426 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
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:
1433 ~
(dsdb
.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1434 dsdb
.NTDSCONN_OPT_USE_NOTIFY
)
1435 cn
.set_modified(True)
1440 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
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:
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
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)
1465 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
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
1475 if cn
.is_intersite_compression_disabled():
1477 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1479 # Perform an originating update to clear bit
1480 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1483 dsdb
.NTDSSITELINK_OPT_DISABLE_COMPRESSION
) == 0):
1485 ~dsdb
.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1486 cn
.set_modified(True)
1490 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1492 # Perform an originating update to set bit
1493 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1496 dsdb
.NTDSSITELINK_OPT_DISABLE_COMPRESSION
) != 0):
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)
1508 ldsa
.commit_connections(self
.samdb
)
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
)
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
)
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
1569 # SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in 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():
1588 logger
.info("TO BE ADDED:\n%s" % cn
)
1590 lbh
.commit_connections(self
.samdb
, ro
=True)
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)
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
,
1631 vertex
.is_black(), detect_failed
)
1633 if vertex
.site
.is_rodc_site():
1634 vertex
.accept_red_red
.append(t_guid
)
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")
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
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
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
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
:
1688 if self
.add_transports(v
, my_vertex
, graph
, detect_failed
):
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
,
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
)
1714 # XXX more accurate comparison?
1715 if e
.directed
and e
.vertices
[0].site
is self
.my_site
:
1718 if e
.vertices
[0].site
is self
.my_site
:
1719 rsite
= e
.vertices
[1].site
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")
1729 # Determine bridgehead server in remote site
1730 rbh
= self
.get_bridgehead(rsite
, part
, transport
,
1731 partial_ok
, detect_failed
)
1735 # RODC acts as an BH for itself
1737 # LET lbh be the nTDSDSA object of the local DC
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
1745 lsite
= self
.my_site
1746 lbh
= self
.get_bridgehead(lsite
, part
, transport
,
1747 partial_ok
, detect_failed
)
1750 debug
.DEBUG_RED("DISASTER! lbh is None")
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:
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
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
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():
1805 if part
.is_foreign():
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
,
1817 DEBUG("with detect_failed: connected %s Found failed %s" %
1818 (connected
, found_failed
))
1820 all_connected
= False
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
1840 After this has run, all sites should be connected in a minimum
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
1850 mysite
= self
.my_site
1851 all_connected
= True
1853 DEBUG_FN("intersite(): enter")
1855 # Determine who is the ISTG
1857 mysite
.select_istg(self
.samdb
, mydsa
, ro
=True)
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" %
1865 return all_connected
1867 if not mydsa
.is_istg():
1868 DEBUG_FN("intersite(): exit not istg all_connected=%d" %
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():
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.
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
1956 :param node_count: the number of nodes in the site
1957 "return: The desired maximum number of connections
1961 if node_count
<= (2 * (n
* n
) + (6 * n
) + 7):
1969 def construct_intrasite_graph(self
, site_local
, dc_local
,
1970 nc_x
, gc_only
, detect_stale
):
1971 """Create an intrasite graph using given parameters
1973 This might be called a number of times per site with different
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
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
1995 # r_list - replica list R
1996 # p_of_x - replica (p) is partial and found on a DC (s)
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
+
2011 "\n\tpartial=%s" % partial
+
2015 debug
.DEBUG_RED("%s lacks 'should be present' status, "
2016 "aborting construct_intrasite_graph!" %
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
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
)
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
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.
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
:
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
2075 # Replica (f) of NC (x) must satisfy the
2076 # "is present" criteria for DC (s) that
2078 if not f_of_x
.is_present():
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
:
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():
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
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
):
2115 # If we haven't been told to turn off stale connection
2116 # detection and this dsa has a stale connection then
2118 if detect_stale
and self
.is_stale_link_connection(dc_s
):
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
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
2146 if nc_x
.nc_dnstr
not in dc_s
.current_rep_table
:
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():
2157 # Replica (p) of NC (x) must satisfy the
2158 # "is present" criteria for DC (s) that
2160 if not p_of_x
.is_present():
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
:
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():
2175 # If we haven't been told to turn off stale connection
2176 # detection and this dsa has a stale connection then
2178 if detect_stale
and self
.is_stale_link_connection(dc_s
):
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"
2187 r_list
.append(l_of_x
)
2189 r_list
.sort(key
=lambda rep
: ndr_pack(rep
.rep_dsa_guid
))
2192 max_node_edges
= self
.intrasite_max_node_edges(r_len
)
2194 # Add a node for each r_list element to the replica graph
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)
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
)
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
))
2228 do_dot_files
= self
.dot_file_dir
is not None and self
.debug
2229 if self
.verify
or do_dot_files
:
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
],
2243 properties
=verify_properties
, debug
=DEBUG
,
2245 dot_file_dir
=self
.dot_file_dir
,
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
,
2256 label
='%s__%s__%s' % (site_local
.site_dnstr
,
2257 nctype_lut
[nc_x
.nc_type
],
2259 properties
=rw_verify_properties
, debug
=DEBUG
,
2261 dot_file_dir
=self
.dot_file_dir
,
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
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
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
),
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
)
2311 DEBUG_FN("not adding links to %s: nodes %s, links is %s/%s" %
2312 (tnode
.dsa_dnstr
, r_len
, len(tnode
.edge_from
),
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
:
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
],
2338 properties
=verify_properties
, debug
=DEBUG
,
2340 dot_file_dir
=self
.dot_file_dir
,
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
,
2351 label
='%s__%s__%s' % (site_local
.site_dnstr
,
2352 nctype_lut
[nc_x
.nc_type
],
2354 properties
=rw_verify_properties
, debug
=DEBUG
,
2356 dot_file_dir
=self
.dot_file_dir
,
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.
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():
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,
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,
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
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.
2453 self
.load_all_sites()
2454 self
.load_all_partitions()
2455 self
.load_ip_transport()
2456 self
.load_all_sitelinks()
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()])
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
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:
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" %
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:
2500 for dsa
in self
.dsa_by_dnstr
.values():
2501 dot_vertices
.append(dsa
.dsa_dnstr
)
2503 vertex_colours
.append('#cc0000')
2505 vertex_colours
.append('#0000cc')
2506 for con
in dsa
.connect_table
.values():
2507 if con
.is_rodc_topology():
2508 edge_colours
.append('red')
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" %
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:
2557 for site
in self
.site_table
.values():
2558 guid_to_dnstr
.update((str(dsa
.dsa_guid
), dnstr
)
2560 in site
.dsa_table
.items())
2562 self
.plot_all_connections('dsa_initial')
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
)
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
)
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
,
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
):
2632 drs_utils
.drsuapi_connect(dnsname
, self
.lp
, self
.creds
)
2633 except drs_utils
.drsException
:
2638 # These are the published steps (in order) for the
2639 # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2642 self
.refresh_failed_links_connections(ping
)
2648 all_connected
= self
.intersite(ping
)
2651 self
.remove_unneeded_ntdsconn(all_connected
)
2654 self
.translate_ntdsconn()
2657 self
.remove_unneeded_failed_links_connections()
2660 self
.update_rodc_connection()
2662 if self
.verify
or self
.dot_file_dir
is not None:
2663 self
.plot_all_connections('dsa_final',
2666 debug
.DEBUG_MAGENTA("there are %d dsa guids" %
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
)
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
)
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
2724 self
.samdb
= ldif_import_export
.ldif_to_samdb(dburl
, lp
, ldif_file
,
2726 except ldif_import_export
.LdifError
as e
:
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
2749 ldif_import_export
.samdb_to_ldif_file(self
.samdb
, dburl
, lp
, creds
,
2751 except ldif_import_export
.LdifError
as e
: