1# define the KCC object 2# 3# Copyright (C) Dave Craft 2011 4# Copyright (C) Andrew Bartlett 2015 5# 6# Andrew Bartlett's alleged work performed by his underlings Douglas 7# Bagnall and Garming Sam. 8# 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. 13# 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. 18# 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/>. 21 22import random 23import uuid 24 25import itertools 26from samba import unix2nttime, nttime2unix 27from samba import ldb, dsdb, drs_utils 28from samba.auth import system_session 29from samba.samdb import SamDB 30from samba.dcerpc import drsuapi, misc 31 32from samba.kcc.kcc_utils import Site, Partition, Transport, SiteLink 33from samba.kcc.kcc_utils import NCReplica, NCType, nctype_lut, GraphNode 34from samba.kcc.kcc_utils import RepsFromTo, KCCError, KCCFailedObject 35from samba.kcc.graph import convert_schedule_to_repltimes 36 37from samba.ndr import ndr_pack 38 39from samba.kcc.graph_utils import verify_and_dot 40 41from samba.kcc import ldif_import_export 42from samba.kcc.graph import setup_graph, get_spanning_tree_edges 43from samba.kcc.graph import Vertex 44 45from samba.kcc.debug import DEBUG, DEBUG_FN, logger 46from samba.kcc import debug 47from samba.compat import cmp_fn 48 49 50def sort_dsa_by_gc_and_guid(dsa1, dsa2): 51 """Helper to sort DSAs by guid global catalog status 52 53 GC DSAs come before non-GC DSAs, other than that, the guids are 54 sorted in NDR form. 55 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_fn(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid)) 65 66 67def is_smtp_replication_available(): 68 """Can the KCC use SMTP replication? 69 70 Currently always returns false because Samba doesn't implement 71 SMTP transfer for NC changes between DCs. 72 73 :return: Boolean (always False) 74 """ 75 return False 76 77 78class KCC(object): 79 """The Knowledge Consistency Checker class. 80 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 84 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 = {} 103 104 self.get_dsa_by_guidstr = self.dsa_by_guid.get 105 self.get_dsa = self.dsa_by_dnstr.get 106 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() 112 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() 117 118 self.my_dsa_dnstr = None # My dsa DN 119 self.my_dsa = None # My dsa object 120 121 self.my_site_dnstr = None 122 self.my_site = None 123 124 self.samdb = None 125 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 132 133 def load_ip_transport(self): 134 """Loads the inter-site transport objects for Sites 135 136 :return: None 137 :raise KCCError: if no IP transport is found 138 """ 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) 148 149 for msg in res: 150 dnstr = str(msg.dn) 151 152 transport = Transport(dnstr) 153 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.") 160 161 else: 162 logger.warning("Samba KCC does not support the transport " 163 "called %r." % (transport.name,)) 164 165 if self.ip_transport is None: 166 raise KCCError("there doesn't seem to be an IP transport") 167 168 def load_all_sitelinks(self): 169 """Loads the inter-site siteLink objects 170 171 :return: None 172 :raise KCCError: if site-links aren't found 173 """ 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) 182 183 for msg in res: 184 dnstr = str(msg.dn) 185 186 # already loaded 187 if dnstr in self.sitelink_table: 188 continue 189 190 sitelink = SiteLink(dnstr) 191 192 sitelink.load_sitelink(self.samdb) 193 194 # Assign this siteLink to table 195 # and index by dn 196 self.sitelink_table[dnstr] = sitelink 197 198 def load_site(self, dn_str): 199 """Helper for load_my_site and load_all_sites. 200 201 Put all the site's DSAs into the KCC indices. 202 203 :param dn_str: a site dn_str 204 :return: the Site object pertaining to the dn_str 205 """ 206 site = Site(dn_str, self.unix_now) 207 site.load_site(self.samdb) 208 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()) 218 219 return self.site_table[guid] 220 221 def load_my_site(self): 222 """Load the Site object for the local DSA. 223 224 :return: None 225 """ 226 self.my_site_dnstr = ("CN=%s,CN=Sites,%s" % ( 227 self.samdb.server_site_name(), 228 self.samdb.get_config_basedn())) 229 230 self.my_site = self.load_site(self.my_site_dnstr) 231 232 def load_all_sites(self): 233 """Discover all sites and create Site objects. 234 235 :return: None 236 :raise: KCCError if sites can't be found 237 """ 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) 246 247 for msg in res: 248 sitestr = str(msg.dn) 249 self.load_site(sitestr) 250 251 def load_my_dsa(self): 252 """Discover my nTDSDSA dn thru the rootDSE entry 253 254 :return: None 255 :raise: KCCError if DSA can't be found 256 """ 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')) 279 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) 285 286 if len(res) != 1: 287 raise KCCError("Unable to find my nTDSDSA at %s" % 288 dn.extended_str()) 289 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") 294 295 self.my_dsa_dnstr = str(res[0].dn) 296 297 self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr) 298 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) 305 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 308 309 def load_all_partitions(self): 310 """Discover and load all partitions. 311 312 Each NC is inserted into the part_table by partition 313 dn string (not the nCName dn string) 314 315 :return: None 316 :raise: KCCError if partitions can't be found 317 """ 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) 326 327 for msg in res: 328 partstr = str(msg.dn) 329 330 # already loaded 331 if partstr in self.part_table: 332 continue 333 334 part = Partition(partstr) 335 336 part.load_partition(self.samdb) 337 self.part_table[partstr] = part 338 339 def refresh_failed_links_connections(self, ping=None): 340 """Ensure the failed links list is up to date 341 342 Based on MS-ADTS 6.2.2.1 343 344 :param ping: An oracle function of remote site availability 345 :return: None 346 """ 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 356 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 361 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 373 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") 387 388 # Remove the restored connections from the failed connections 389 self.kcc_failed_connections.difference_update(restore_connections) 390 391 def is_stale_link_connection(self, target_dsa): 392 """Check whether a link to a remote DSA is stale 393 394 Used in MS-ADTS 6.2.2.2 Intrasite Connection Creation 395 396 Returns True if the remote seems to have been down for at 397 least two hours, otherwise False. 398 399 :param target_dsa: the remote DSA object 400 :return: True if link is stale, otherwise False 401 """ 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!") 412 413 # Perform calculation in seconds 414 if (self.unix_now - unix_first_failure) > 60 * 60 * 2: 415 return True 416 417 # TODO connections. 418 # We have checked failed *links*, but we also need to check 419 # *connections* 420 421 return False 422 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. 427 428 # Remove all connections which were not used this run or connections 429 # that became active during this run. 430 pass 431 432 def _ensure_connections_are_loaded(self, connections): 433 """Load or fake-load NTDSConnections lacking GUIDs 434 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. 438 439 :param connections: an iterable of NTDSConnection objects. 440 :return: None 441 """ 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) 449 450 def _mark_broken_ntdsconn(self): 451 """Find NTDS Connections that lack a remote 452 453 I'm not sure how they appear. Let's be rid of them by marking 454 them with the to_be_deleted attribute. 455 456 :return: None 457 """ 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 464 465 def _mark_unneeded_local_ntdsconn(self): 466 """Find unneeded intrasite NTDS Connections for removal 467 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. 471 472 :return: None 473 """ 474 # XXX should an RODC be regarded as same site? It isn't part 475 # of the intrasite ring. 476 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 481 482 mydsa = self.my_dsa 483 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 490 491 local_connections = [] 492 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)) 501 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 506 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 516 517 def _mark_unneeded_intersite_ntdsconn(self): 518 """find unneeded intersite NTDS Connections for removal 519 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. 523 524 :return: None 525 """ 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 530 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)) 550 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 555 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 562 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 573 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) 583 584 # Peform 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) 590 591 def remove_unneeded_ntdsconn(self, all_connected): 592 """Remove unneeded NTDS Connections once topology is calculated 593 594 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections 595 596 :param all_connected: indicates whether all sites are connected 597 :return: None 598 """ 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() 605 606 for dsa in self.my_site.dsa_table.values(): 607 self._commit_changes(dsa) 608 609 def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn): 610 """Update an repsFrom object if required. 611 612 Part of MS-ADTS 6.2.2.5. 613 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. 619 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. 623 624 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 630 631 :return: None 632 """ 633 s_dnstr = s_dsa.dsa_dnstr 634 same_site = s_dnstr in self.my_site.dsa_table 635 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 640 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 646 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(): 652 653 if ((t_repsFrom.replica_flags & 654 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0): 655 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC 656 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): 662 663 if ((t_repsFrom.replica_flags & 664 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0): 665 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC 666 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): 675 676 if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0: 677 # WARNING 678 # 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 690 691 elif not same_site: 692 693 if ((t_repsFrom.replica_flags & 694 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0): 695 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY 696 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): 705 706 if ((t_repsFrom.replica_flags & 707 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0): 708 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION 709 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: 713 714 if ((t_repsFrom.replica_flags & 715 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0): 716 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC 717 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(): 721 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 726 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 731 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: 735 # 736 # Bit DRS_MAIL_REP in t.replicaFlags is clear. 737 # 738 # t.uuidTransport = NULL GUID. 739 # 740 # t.uuidDsa = The GUID-based DNS name of s. 741 # 742 # Otherwise: 743 # 744 # Bit DRS_MAIL_REP in t.replicaFlags is set. 745 # 746 # If x is the object with dsname cn!transportType, 747 # t.uuidTransport = x!objectGUID. 748 # 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. 753 # 754 # It appears that the first statement i.e. 755 # 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:" 759 # 760 # could be a slightly tighter statement if it had an "or" 761 # between each condition. I believe this should 762 # be interpreted as: 763 # 764 # IF (same-site) OR (no-value) OR (type-ip) 765 # 766 # because IP should be the primary transport mechanism 767 # (even in inter-site) and the absense of the transportType 768 # attribute should always imply IP no matter if its multi-site 769 # 770 # NOTE MS-TECH INCORRECT: 771 # 772 # All indications point to these statements above being 773 # incorrectly stated: 774 # 775 # t.uuidDsa = The GUID-based DNS name of s. 776 # 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. 781 # 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: 785 # 786 # t.naDsa = The GUID-based DNS name of s 787 # 788 # That would also be correct if transportAddressAttribute 789 # were "mailAddress" because (naDsa) can also correctly 790 # hold the SMTP ISM service address. 791 # 792 nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name()) 793 794 if ((t_repsFrom.replica_flags & 795 drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0): 796 t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP 797 798 t_repsFrom.transport_guid = misc.GUID() 799 800 # See (NOTE MS-TECH INCORRECT) above 801 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 809 810 if t_repsFrom.version > 0x1 and t_repsFrom.dns_name2 != nastr: 811 t_repsFrom.dns_name2 = nastr 812 813 if t_repsFrom.is_modified(): 814 DEBUG_FN("modify_repsFrom(): %s" % t_repsFrom) 815 816 def get_dsa_for_implied_replica(self, n_rep, cn_conn): 817 """If a connection imply a replica, find the relevant DSA 818 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. 823 824 Based on part of MS-ADTS 6.2.2.5 825 826 :param n_rep: NC replica 827 :param cn_conn: NTDS Connection 828 :return: source DSA or None 829 """ 830 # XXX different conditions for "implies" than MS-ADTS 6.2.2 831 # preamble. 832 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. 836 837 if not cn_conn.is_enabled() or cn_conn.is_rodc_topology(): 838 return None 839 840 s_dnstr = cn_conn.get_from_dnstr() 841 s_dsa = self.get_dsa(s_dnstr) 842 843 # No DSA matching this source DN string? 844 if s_dsa is None: 845 return None 846 847 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr) 848 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 854 855 def translate_ntdsconn(self, current_dsa=None): 856 """Adjust repsFrom to match NTDSConnections 857 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. 861 862 Based on [MS-ADTS] 6.2.2.5 863 864 :param current_dsa: optional DSA on whose behalf we are acting. 865 :return: None 866 """ 867 ro = False 868 if current_dsa is None: 869 current_dsa = self.my_dsa 870 871 if current_dsa.is_ro(): 872 ro = True 873 874 if current_dsa.is_translate_ntdsconn_disabled(): 875 DEBUG_FN("skipping translate_ntdsconn() " 876 "because disabling flag is set") 877 return 878 879 DEBUG_FN("translate_ntdsconn(): enter") 880 881 current_rep_table, needed_rep_table = current_dsa.get_rep_tables() 882 883 # Filled in with replicas we currently have that need deleting 884 delete_reps = set() 885 886 # We're using the MS notation names here to allow 887 # correlation back to the published algorithm. 888 # 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 898 # 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) 918 919 DEBUG_FN('current %d needed %d delete %d' % (len(current_rep_table), 920 len(needed_rep_table), len(delete_reps))) 921 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] 927 928 # HANDLE REPS-FROM 929 # 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(): 934 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) 939 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: 944 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) 949 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 957 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 970 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 975 # 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) 980 981 if s_rep is None or not s_rep.is_present() or \ 982 (not n_rep.is_ro() and s_rep.is_partial()): 983 984 t_repsFrom.to_be_deleted = True 985 continue 986 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) 989 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(): 994 995 s_dsa = self.get_dsa_for_implied_replica(n_rep, cn_conn) 996 if s_dsa is None: 997 continue 998 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 1008 1009 if s_dsa is None: 1010 continue 1011 1012 # Create a new RepsFromTo and proceed to modify 1013 # it according to specification 1014 t_repsFrom = RepsFromTo(n_rep.nc_dnstr) 1015 1016 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid 1017 1018 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr) 1019 1020 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn) 1021 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) 1025 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) 1034 1035 # Peform 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) 1041 1042 # HANDLE REPS-TO: 1043 # 1044 # Now perform the scan of replicas we'll need 1045 # and compare any current repsTo against the 1046 # connections 1047 1048 # RODC should never push to anybody (should we check this?) 1049 if ro: 1050 return 1051 1052 for n_rep in needed_rep_table.values(): 1053 1054 # load any repsTo and fsmo roles as we'll 1055 # need them during connection translation 1056 n_rep.load_repsTo(self.samdb) 1057 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: 1062 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) 1067 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 1075 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 1085 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 1093 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) 1099 1100 # Peform 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) 1106 1107 # TODO Remove any duplicate repsTo values. This should never happen in 1108 # any normal situations. 1109 1110 def merge_failed_links(self, ping=None): 1111 """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads. 1112 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. 1116 1117 Based on MS-ADTS 6.2.2.3.2 "Merge of kCCFailedLinks and kCCFailedLinks 1118 from Bridgeheads" 1119 1120 :param ping: An oracle of current bridgehead availability 1121 :return: None 1122 """ 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 1126 # 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") 1134 1135 def setup_graph(self, part): 1136 """Set up an intersite graph 1137 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. 1143 1144 Roughly corresponds to MS-ADTS 6.2.2.3.4.3 1145 1146 :param part: a Partition object 1147 :returns: an InterSiteGraph object 1148 """ 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) 1156 1157 g = setup_graph(part, self.site_table, transport_guid, 1158 self.sitelink_table, bridges_required) 1159 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) 1172 1173 return g 1174 1175 def get_bridgehead(self, site, part, transport, partial_ok, detect_failed): 1176 """Get a bridghead DC for a site. 1177 1178 Part of MS-ADTS 6.2.2.3.4.4 1179 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 1192 """ 1193 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 1200 1201 debug.DEBUG_GREEN("get_bridgehead:\n\tsitedn = %s\n\tbhdn = %s" % 1202 (site.site_dnstr, bhs[0].dsa_dnstr)) 1203 return bhs[0] 1204 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 1208 1209 Part of MS-ADTS 6.2.2.3.4.4 1210 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 1223 """ 1224 bhs = [] 1225 1226 if transport.name != "IP": 1227 raise KCCError("get_all_bridgeheads has run into a " 1228 "non-IP transport! %r" 1229 % (transport.name,)) 1230 1231 DEBUG_FN(site.rw_dsa_table) 1232 for dsa in site.rw_dsa_table.values(): 1233 1234 pdnstr = dsa.get_parent_dnstr() 1235 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 1242 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) 1253 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 1263 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 1271 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 1277 1278 DEBUG_FN("found a bridgehead: %s" % dsa.dsa_dnstr) 1279 bhs.append(dsa) 1280 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(sort_dsa_by_gc_and_guid) 1289 else: 1290 random.shuffle(bhs) 1291 debug.DEBUG_YELLOW(bhs) 1292 return bhs 1293 1294 def is_bridgehead_failed(self, dsa, detect_failed): 1295 """Determine whether a given DC is known to be in a failed state 1296 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 1300 1301 Here we DEPART from the pseudo code spec which appears to be 1302 wrong. It says, in full: 1303 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. 1311 */ 1312 BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool 1313 { 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 1326 } 1327 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. 1333 """ 1334 if not detect_failed: 1335 return False 1336 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 1342 1343 return self.is_stale_link_connection(dsa) 1344 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. 1349 1350 Part of MS-ADTS 6.2.2.3.4.5 1351 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. 1371 """ 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) 1375 1376 debug.DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all), 1377 [x.dsa_dnstr for x in rbhs_all])) 1378 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) 1382 1383 lbhs_all = self.get_all_bridgeheads(lsite, part, transport, 1384 partial_ok, False) 1385 if lbh.is_ro(): 1386 lbhs_all.append(lbh) 1387 1388 debug.DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all), 1389 [x.dsa_dnstr for x in lbhs_all])) 1390 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) 1394 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(): 1399 1400 rdsa = rbh_table.get(cn.from_dnstr) 1401 if rdsa is None: 1402 continue 1403 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)): 1411 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) 1420 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(): 1425 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) 1436 1437 # ELSE 1438 else: 1439 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) 1450 1451 # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options 1452 if cn.is_twoway_sync(): 1453 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) 1461 1462 # ELSE 1463 else: 1464 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) 1472 1473 # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set 1474 # in cn!options 1475 if cn.is_intersite_compression_disabled(): 1476 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) 1487 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) 1500 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) 1505 1506 ldsa.commit_connections(self.samdb, ro=True) 1507 else: 1508 ldsa.commit_connections(self.samdb) 1509 # ENDFOR 1510 1511 valid_connections = 0 1512 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(): 1517 1518 rdsa = rbh_table.get(cn.from_dnstr) 1519 if rdsa is None: 1520 continue 1521 1522 debug.DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr) 1523 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())): 1530 1531 # LET rguid be the objectGUID of the nTDSDSA object 1532 # referenced by cn!fromServer 1533 # LET lguid be (cn!parent)!objectGUID 1534 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 1541 1542 # IF keepConnections does not contain cn!objectGUID 1543 # APPEND cn!objectGUID to keepConnections 1544 self.kept_connections.add(cn) 1545 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: 1551 1552 # LET opt be NTDSCONN_OPT_IS_GENERATED 1553 opt = dsdb.NTDSCONN_OPT_IS_GENERATED 1554 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) 1561 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 1566 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 1573 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) 1581 1582 cn = lbh.new_connection(opt, system_flags, transport, 1583 rbh.dsa_dnstr, link_sched) 1584 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) 1589 1590 lbh.commit_connections(self.samdb, ro=True) 1591 else: 1592 lbh.commit_connections(self.samdb) 1593 1594 # APPEND cn!objectGUID to keepConnections 1595 self.kept_connections.add(cn) 1596 1597 def add_transports(self, vertex, local_vertex, graph, detect_failed): 1598 """Build a Vertex's transport lists 1599 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". 1604 1605 Part of MS-ADTS 6.2.2.3.4.3 -- ColorVertices 1606 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 1612 """ 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: 1616 # 1617 # bh = self.get_bridgehead(local_vertex.site, vertex.part, transport, 1618 # local_vertex.is_black(), detect_failed) 1619 # 1620 # TODO WHY????? 1621 1622 vertex.accept_red_red = [] 1623 vertex.accept_black = [] 1624 found_failed = False 1625 1626 if vertex in graph.connected_vertices: 1627 t_guid = str(self.ip_transport.guid) 1628 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) 1640 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") 1644 1645 return found_failed 1646 1647 def create_connections(self, graph, part, detect_failed): 1648 """Create intersite NTDSConnections as needed by a partition 1649 1650 Construct an NC replica graph for the NC identified by 1651 the given crossRef, then create any additional nTDSConnection 1652 objects required. 1653 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. 1659 1660 Modifies self.kept_connections by adding any connections 1661 deemed to be "in use". 1662 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. 1668 """ 1669 all_connected = True 1670 found_failed = False 1671 1672 DEBUG_FN("create_connections(): enter\n" 1673 "\tpartdn=%s\n\tdetect_failed=%s" % 1674 (part.nc_dnstr, detect_failed)) 1675 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. 1682 1683 my_vertex = Vertex(self.my_site, part) 1684 my_vertex.color_vertex() 1685 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 1690 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 1695 1696 edge_list, n_components = get_spanning_tree_edges(graph, 1697 self.my_site, 1698 label=part.partstr) 1699 1700 DEBUG_FN("%s Number of components: %d" % 1701 (part.nc_dnstr, n_components)) 1702 if n_components > 1: 1703 all_connected = False 1704 1705 # LET partialReplicaOkay be TRUE if and only if 1706 # localSiteVertex.Color = COLOR.BLACK 1707 partial_ok = my_vertex.is_black() 1708 1709 # Utilize the IP transport only for now 1710 transport = self.ip_transport 1711 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 1717 1718 if e.vertices[0].site is self.my_site: 1719 rsite = e.vertices[1].site 1720 else: 1721 rsite = e.vertices[0].site 1722 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 1728 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 1734 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 1752 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)) 1756 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 1764 1765 self.create_connection(part, rbh, rsite, transport, 1766 lbh, lsite, link_opt, link_sched, 1767 partial_ok, detect_failed) 1768 1769 return all_connected, found_failed 1770 1771 def create_intersite_connections(self): 1772 """Create NTDSConnections as necessary for all partitions. 1773 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. 1780 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. 1784 1785 :return: True if spanning trees were created for all NC replica 1786 graphs, otherwise False. 1787 """ 1788 all_connected = True 1789 self.kept_connections = set() 1790 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 1794 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() 1799 1800 for part in self.part_table.values(): 1801 1802 if not part.is_enabled(): 1803 continue 1804 1805 if part.is_foreign(): 1806 continue 1807 1808 graph = self.setup_graph(part) 1809 1810 # Create nTDSConnection objects, routing replication traffic 1811 # around "failed" DCs. 1812 found_failed = False 1813 1814 connected, found_failed = self.create_connections(graph, 1815 part, True) 1816 1817 DEBUG("with detect_failed: connected %s Found failed %s" % 1818 (connected, found_failed)) 1819 if not connected: 1820 all_connected = False 1821 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) 1826 1827 return all_connected 1828 1829 def intersite(self, ping): 1830 """Generate the inter-site KCC replica graph and nTDSConnections 1831 1832 As per MS-ADTS 6.2.2.3. 1833 1834 If self.readonly is False, the connections are added to self.samdb. 1835 1836 Produces self.kept_connections which is a set of NTDS 1837 Connections that should be kept during subsequent pruning 1838 process. 1839 1840 After this has run, all sites should be connected in a minimum 1841 spanning tree. 1842 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 1846 """ 1847 1848 # Retrieve my DSA 1849 mydsa = self.my_dsa 1850 mysite = self.my_site 1851 all_connected = True 1852 1853 DEBUG_FN("intersite(): enter") 1854 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) 1860 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 1866 1867 if not mydsa.is_istg(): 1868 DEBUG_FN("intersite(): exit not istg all_connected=%d" % 1869 all_connected) 1870 return all_connected 1871 1872 self.merge_failed_links(ping) 1873 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. 1880 1881 all_connected = self.create_intersite_connections() 1882 1883 DEBUG_FN("intersite(): exit all_connected=%d" % all_connected) 1884 return all_connected 1885 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. 1890 1891 If the local DSA is not an RODC, this does nothing. 1892 """ 1893 if not self.my_dsa.is_ro(): 1894 return 1895 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: 1900 # 1901 # cn1.fromServer = cn2.fromServer 1902 # cn1.schedule = cn2.schedule 1903 # 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. 1906 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] 1911 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 1921 1922 self.my_dsa.commit_connections(self.samdb, ro=ro) 1923 1924 def intrasite_max_node_edges(self, node_count): 1925 """Find the maximum number of edges directed to an intrasite node 1926 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) 1932 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. 1937 1938 edges n nodecount 1939 2 0 7 1940 3 1 15 1941 4 2 27 1942 5 3 43 1943 ... 1944 50 48 4903 1945 1946 :param node_count: total number of nodes in the replica graph 1947 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. 1955 1956 :param node_count: the number of nodes in the site 1957 "return: The desired maximum number of connections 1958 """ 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 1968 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 1972 1973 This might be called a number of times per site with different 1974 parameters. 1975 1976 Based on [MS-ADTS] 6.2.2.2 1977 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 1985 """ 1986 # We're using the MS notation names here to allow 1987 # correlation back to the published algorithm. 1988 # 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| 2001 # 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) 2005 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) 2013 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 2019 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) 2023 2024 l_of_x.identify_by_basedn(self.samdb) 2025 2026 l_of_x.rep_partial = partial 2027 l_of_x.rep_ro = ro 2028 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) 2032 2033 # Replica list 2034 # 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: 2038 # 2039 # * s is a writable DC other than the local DC. 2040 # 2041 # * s is in the same site as the local DC. 2042 # 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. 2046 # 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. 2054 2055 r_list = [] 2056 2057 # We'll loop thru all the DSAs looking for 2058 # writeable NC replicas that match the naming 2059 # context dn for (nc_x) 2060 # 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 2066 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] 2070 2071 # Replica (f) of NC (x) must be writable 2072 if f_of_x.is_ro(): 2073 continue 2074 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 2080 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 2086 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 2092 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) 2097 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 2101 # 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. 2106 # 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 2114 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 2120 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) 2124 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. 2129 # 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 # doen'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. 2136 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(): 2142 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 2148 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] 2152 2153 # Replica (p) of NC (x) must be partial 2154 if not p_of_x.is_partial(): 2155 continue 2156 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 2162 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 2168 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 2174 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 2180 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) 2184 2185 # Append to R the NC replica that "should be present" 2186 # on the local DC 2187 r_list.append(l_of_x) 2188 2189 r_list.sort(key=lambda rep: ndr_pack(rep.rep_dsa_guid)) 2190 r_len = len(r_list) 2191 2192 max_node_edges = self.intrasite_max_node_edges(r_len) 2193 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) 2199 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) 2207 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 2213 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) 2218 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) 2223 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)) 2227 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) 2237 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) 2247 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) 2263 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) 2274 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)) 2277 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. 2285 # 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)] 2296 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))) 2301 2302 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates]) 2303 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)) 2314 2315 # Print the graph node in debug mode 2316 DEBUG_FN("%s" % tnode) 2317 2318 # For each edge directed to the local DC, ensure a nTDSConnection 2319 # points to us that satisfies the KCC criteria 2320 2321 if tnode.dsa_dnstr == dc_local.dsa_dnstr: 2322 tnode.add_connections_from_edges(dc_local, self.ip_transport) 2323 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) 2332 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) 2342 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) 2358 2359 def intrasite(self): 2360 """Generate the intrasite KCC connections 2361 2362 As per MS-ADTS 6.2.2.2. 2363 2364 If self.readonly is False, the connections are added to self.samdb. 2365 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. 2370 2371 :return: None 2372 """ 2373 mydsa = self.my_dsa 2374 2375 DEBUG_FN("intrasite(): enter") 2376 2377 # Test whether local site has topology disabled 2378 mysite = self.my_site 2379 if mysite.is_intrasite_topology_disabled(): 2380 return 2381 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) 2386 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) 2394 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) 2402 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) 2408 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) 2418 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 2423 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) 2431 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 2436 2437 self._commit_changes(mydsa) 2438 2439 def list_dsas(self): 2440 """Compile a comprehensive list of DSA DNs 2441 2442 These are all the DSAs on all the sites that KCC would be 2443 dealing with. 2444 2445 This method is not idempotent and may not work correctly in 2446 sequence with KCC.run(). 2447 2448 :return: a list of DSA DN strings. 2449 """ 2450 self.load_my_site() 2451 self.load_my_dsa() 2452 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 2462 2463 def load_samdb(self, dburl, lp, creds, force=False): 2464 """Load the database using an url, loadparm, and credentials 2465 2466 If force is False, the samdb won't be reloaded if it already 2467 exists. 2468 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. 2473 2474 """ 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)) 2484 2485 def plot_all_connections(self, basename, verify_properties=()): 2486 """Helper function to plot and verify NTDSConnections 2487 2488 :param basename: an identifying string to use in filenames and logs. 2489 :param verify_properties: properties to verify (default empty) 2490 """ 2491 verify = verify_properties and self.verify 2492 if not verify and self.dot_file_dir is None: 2493 return 2494 2495 dot_edges = [] 2496 dot_vertices = [] 2497 edge_colours = [] 2498 vertex_colours = [] 2499 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)) 2512 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) 2519 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 2524 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 2536 """ 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) 2540 2541 if forced_local_dsa: 2542 self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" % 2543 forced_local_dsa) 2544 2545 try: 2546 # Setup 2547 self.load_my_site() 2548 self.load_my_dsa() 2549 2550 self.load_all_sites() 2551 self.load_all_partitions() 2552 self.load_ip_transport() 2553 self.load_all_sitelinks() 2554 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()) 2561 2562 self.plot_all_connections('dsa_initial') 2563 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)) 2569 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) 2574 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)) 2585 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) 2590 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) 2607 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') 2616 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()) 2624 2625 self.plot_all_connections('dsa_forgotten_all') 2626 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) 2640 2641 # Step 1 2642 self.refresh_failed_links_connections(ping) 2643 2644 # Step 2 2645 self.intrasite() 2646 2647 # Step 3 2648 all_connected = self.intersite(ping) 2649 2650 # Step 4 2651 self.remove_unneeded_ntdsconn(all_connected) 2652 2653 # Step 5 2654 self.translate_ntdsconn() 2655 2656 # Step 6 2657 self.remove_unneeded_failed_links_connections() 2658 2659 # Step 7 2660 self.update_rodc_connection() 2661 2662 if self.verify or self.dot_file_dir is not None: 2663 self.plot_all_connections('dsa_final', 2664 ('connected',)) 2665 2666 debug.DEBUG_MAGENTA("there are %d dsa guids" % 2667 len(guid_to_dnstr)) 2668 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]) 2678 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) 2684 2685 dot_edges = [] 2686 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)) 2695 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) 2700 2701 except: 2702 raise 2703 2704 return 0 2705 2706 def import_ldif(self, dburl, lp, ldif_file, forced_local_dsa=None): 2707 """Import relevant objects and attributes from an LDIF file. 2708 2709 The point of this function is to allow a programmer/debugger to 2710 import an LDIF file with non-security relevent 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. 2716 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 2722 """ 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 2730 2731 def export_ldif(self, dburl, lp, creds, ldif_file): 2732 """Save KCC relevant details to an ldif file 2733 2734 The point of this function is to allow a programmer/debugger to 2735 extract an LDIF file with non-security relevent 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. 2741 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 2747 """ 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 2755