1# KCC topology utilities 2# 3# Copyright (C) Dave Craft 2011 4# Copyright (C) Jelmer Vernooij 2011 5# Copyright (C) Andrew Bartlett 2015 6# 7# Andrew Bartlett's alleged work performed by his underlings Douglas 8# Bagnall and Garming Sam. 9# 10# This program is free software; you can redistribute it and/or modify 11# it under the terms of the GNU General Public License as published by 12# the Free Software Foundation; either version 3 of the License, or 13# (at your option) any later version. 14# 15# This program is distributed in the hope that it will be useful, 16# but WITHOUT ANY WARRANTY; without even the implied warranty of 17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18# GNU General Public License for more details. 19# 20# You should have received a copy of the GNU General Public License 21# along with this program. If not, see <http://www.gnu.org/licenses/>. 22from __future__ import print_function 23import sys 24import ldb 25import uuid 26 27from samba import dsdb 28from samba.dcerpc import ( 29 drsblobs, 30 drsuapi, 31 misc, 32) 33from samba.common import dsdb_Dn 34from samba.ndr import ndr_unpack, ndr_pack 35from collections import Counter 36 37 38class KCCError(Exception): 39 pass 40 41 42class NCType(object): 43 (unknown, schema, domain, config, application) = range(0, 5) 44 45 46# map the NCType enum to strings for debugging 47nctype_lut = dict((v, k) for k, v in NCType.__dict__.items() if k[:2] != '__') 48 49 50class NamingContext(object): 51 """Base class for a naming context. 52 53 Holds the DN, GUID, SID (if available) and type of the DN. 54 Subclasses may inherit from this and specialize 55 """ 56 57 def __init__(self, nc_dnstr): 58 """Instantiate a NamingContext 59 60 :param nc_dnstr: NC dn string 61 """ 62 self.nc_dnstr = nc_dnstr 63 self.nc_guid = None 64 self.nc_sid = None 65 self.nc_type = NCType.unknown 66 67 def __str__(self): 68 '''Debug dump string output of class''' 69 text = "%s:" % (self.__class__.__name__,) +\ 70 "\n\tnc_dnstr=%s" % self.nc_dnstr +\ 71 "\n\tnc_guid=%s" % str(self.nc_guid) 72 73 if self.nc_sid is None: 74 text = text + "\n\tnc_sid=<absent>" 75 else: 76 text = text + "\n\tnc_sid=<present>" 77 78 text = text + "\n\tnc_type=%s (%s)" % (nctype_lut[self.nc_type], 79 self.nc_type) 80 return text 81 82 def load_nc(self, samdb): 83 attrs = ["objectGUID", 84 "objectSid"] 85 try: 86 res = samdb.search(base=self.nc_dnstr, 87 scope=ldb.SCOPE_BASE, attrs=attrs) 88 89 except ldb.LdbError as e: 90 (enum, estr) = e.args 91 raise KCCError("Unable to find naming context (%s) - (%s)" % 92 (self.nc_dnstr, estr)) 93 msg = res[0] 94 if "objectGUID" in msg: 95 self.nc_guid = misc.GUID(samdb.schema_format_value("objectGUID", 96 msg["objectGUID"][0])) 97 if "objectSid" in msg: 98 self.nc_sid = msg["objectSid"][0] 99 100 assert self.nc_guid is not None 101 102 def is_config(self): 103 '''Return True if NC is config''' 104 assert self.nc_type != NCType.unknown 105 return self.nc_type == NCType.config 106 107 def identify_by_basedn(self, samdb): 108 """Given an NC object, identify what type is is thru 109 the samdb basedn strings and NC sid value 110 """ 111 # Invoke loader to initialize guid and more 112 # importantly sid value (sid is used to identify 113 # domain NCs) 114 if self.nc_guid is None: 115 self.load_nc(samdb) 116 117 # We check against schema and config because they 118 # will be the same for all nTDSDSAs in the forest. 119 # That leaves the domain NCs which can be identified 120 # by sid and application NCs as the last identified 121 if self.nc_dnstr == str(samdb.get_schema_basedn()): 122 self.nc_type = NCType.schema 123 elif self.nc_dnstr == str(samdb.get_config_basedn()): 124 self.nc_type = NCType.config 125 elif self.nc_sid is not None: 126 self.nc_type = NCType.domain 127 else: 128 self.nc_type = NCType.application 129 130 def identify_by_dsa_attr(self, samdb, attr): 131 """Given an NC which has been discovered thru the 132 nTDSDSA database object, determine what type of NC 133 it is (i.e. schema, config, domain, application) via 134 the use of the schema attribute under which the NC 135 was found. 136 137 :param attr: attr of nTDSDSA object where NC DN appears 138 """ 139 # If the NC is listed under msDS-HasDomainNCs then 140 # this can only be a domain NC and it is our default 141 # domain for this dsa 142 if attr == "msDS-HasDomainNCs": 143 self.nc_type = NCType.domain 144 145 # If the NC is listed under hasPartialReplicaNCs 146 # this is only a domain NC 147 elif attr == "hasPartialReplicaNCs": 148 self.nc_type = NCType.domain 149 150 # NCs listed under hasMasterNCs are either 151 # default domain, schema, or config. We 152 # utilize the identify_by_basedn() to 153 # identify those 154 elif attr == "hasMasterNCs": 155 self.identify_by_basedn(samdb) 156 157 # Still unknown (unlikely) but for completeness 158 # and for finally identifying application NCs 159 if self.nc_type == NCType.unknown: 160 self.identify_by_basedn(samdb) 161 162 163class NCReplica(NamingContext): 164 """Naming context replica that is relative to a specific DSA. 165 166 This is a more specific form of NamingContext class (inheriting from that 167 class) and it identifies unique attributes of the DSA's replica for a NC. 168 """ 169 170 def __init__(self, dsa, nc_dnstr): 171 """Instantiate a Naming Context Replica 172 173 :param dsa_guid: GUID of DSA where replica appears 174 :param nc_dnstr: NC dn string 175 """ 176 self.rep_dsa_dnstr = dsa.dsa_dnstr 177 self.rep_dsa_guid = dsa.dsa_guid 178 self.rep_default = False # replica for DSA's default domain 179 self.rep_partial = False 180 self.rep_ro = False 181 self.rep_instantiated_flags = 0 182 183 self.rep_fsmo_role_owner = None 184 185 # RepsFromTo tuples 186 self.rep_repsFrom = [] 187 188 # RepsFromTo tuples 189 self.rep_repsTo = [] 190 191 # The (is present) test is a combination of being 192 # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or 193 # hasPartialReplicaNCs) as well as its replica flags found 194 # thru the msDS-HasInstantiatedNCs. If the NC replica meets 195 # the first enumeration test then this flag is set true 196 self.rep_present_criteria_one = False 197 198 # Call my super class we inherited from 199 NamingContext.__init__(self, nc_dnstr) 200 201 def __str__(self): 202 '''Debug dump string output of class''' 203 text = "%s:" % self.__class__.__name__ +\ 204 "\n\tdsa_dnstr=%s" % self.rep_dsa_dnstr +\ 205 "\n\tdsa_guid=%s" % self.rep_dsa_guid +\ 206 "\n\tdefault=%s" % self.rep_default +\ 207 "\n\tro=%s" % self.rep_ro +\ 208 "\n\tpartial=%s" % self.rep_partial +\ 209 "\n\tpresent=%s" % self.is_present() +\ 210 "\n\tfsmo_role_owner=%s" % self.rep_fsmo_role_owner +\ 211 "".join("\n%s" % rep for rep in self.rep_repsFrom) +\ 212 "".join("\n%s" % rep for rep in self.rep_repsTo) 213 214 return "%s\n%s" % (NamingContext.__str__(self), text) 215 216 def set_instantiated_flags(self, flags=0): 217 '''Set or clear NC replica instantiated flags''' 218 self.rep_instantiated_flags = flags 219 220 def identify_by_dsa_attr(self, samdb, attr): 221 """Given an NC which has been discovered thru the 222 nTDSDSA database object, determine what type of NC 223 replica it is (i.e. partial, read only, default) 224 225 :param attr: attr of nTDSDSA object where NC DN appears 226 """ 227 # If the NC was found under hasPartialReplicaNCs 228 # then a partial replica at this dsa 229 if attr == "hasPartialReplicaNCs": 230 self.rep_partial = True 231 self.rep_present_criteria_one = True 232 233 # If the NC is listed under msDS-HasDomainNCs then 234 # this can only be a domain NC and it is the DSA's 235 # default domain NC 236 elif attr == "msDS-HasDomainNCs": 237 self.rep_default = True 238 239 # NCs listed under hasMasterNCs are either 240 # default domain, schema, or config. We check 241 # against schema and config because they will be 242 # the same for all nTDSDSAs in the forest. That 243 # leaves the default domain NC remaining which 244 # may be different for each nTDSDSAs (and thus 245 # we don't compare agains this samdb's default 246 # basedn 247 elif attr == "hasMasterNCs": 248 self.rep_present_criteria_one = True 249 250 if self.nc_dnstr != str(samdb.get_schema_basedn()) and \ 251 self.nc_dnstr != str(samdb.get_config_basedn()): 252 self.rep_default = True 253 254 # RODC only 255 elif attr == "msDS-hasFullReplicaNCs": 256 self.rep_present_criteria_one = True 257 self.rep_ro = True 258 259 # Not RODC 260 elif attr == "msDS-hasMasterNCs": 261 self.rep_present_criteria_one = True 262 self.rep_ro = False 263 264 # Now use this DSA attribute to identify the naming 265 # context type by calling the super class method 266 # of the same name 267 NamingContext.identify_by_dsa_attr(self, samdb, attr) 268 269 def is_default(self): 270 """Whether this is a default domain for the dsa that this NC appears on 271 """ 272 return self.rep_default 273 274 def is_ro(self): 275 '''Return True if NC replica is read only''' 276 return self.rep_ro 277 278 def is_partial(self): 279 '''Return True if NC replica is partial''' 280 return self.rep_partial 281 282 def is_present(self): 283 """Given an NC replica which has been discovered thru the 284 nTDSDSA database object and populated with replica flags 285 from the msDS-HasInstantiatedNCs; return whether the NC 286 replica is present (true) or if the IT_NC_GOING flag is 287 set then the NC replica is not present (false) 288 """ 289 if self.rep_present_criteria_one and \ 290 self.rep_instantiated_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0: 291 return True 292 return False 293 294 def load_repsFrom(self, samdb): 295 """Given an NC replica which has been discovered thru the nTDSDSA 296 database object, load the repsFrom attribute for the local replica. 297 held by my dsa. The repsFrom attribute is not replicated so this 298 attribute is relative only to the local DSA that the samdb exists on 299 """ 300 try: 301 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE, 302 attrs=["repsFrom"]) 303 304 except ldb.LdbError as e1: 305 (enum, estr) = e1.args 306 raise KCCError("Unable to find NC for (%s) - (%s)" % 307 (self.nc_dnstr, estr)) 308 309 msg = res[0] 310 311 # Possibly no repsFrom if this is a singleton DC 312 if "repsFrom" in msg: 313 for value in msg["repsFrom"]: 314 try: 315 unpacked = ndr_unpack(drsblobs.repsFromToBlob, value) 316 except RuntimeError as e: 317 print("bad repsFrom NDR: %r" % (value), 318 file=sys.stderr) 319 continue 320 rep = RepsFromTo(self.nc_dnstr, unpacked) 321 self.rep_repsFrom.append(rep) 322 323 def commit_repsFrom(self, samdb, ro=False): 324 """Commit repsFrom to the database""" 325 326 # XXX - This is not truly correct according to the MS-TECH 327 # docs. To commit a repsFrom we should be using RPCs 328 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and 329 # IDL_DRSReplicaDel to affect a repsFrom change. 330 # 331 # Those RPCs are missing in samba, so I'll have to 332 # implement them to get this to more accurately 333 # reflect the reference docs. As of right now this 334 # commit to the database will work as its what the 335 # older KCC also did 336 modify = False 337 newreps = [] 338 delreps = [] 339 340 for repsFrom in self.rep_repsFrom: 341 342 # Leave out any to be deleted from 343 # replacement list. Build a list 344 # of to be deleted reps which we will 345 # remove from rep_repsFrom list below 346 if repsFrom.to_be_deleted: 347 delreps.append(repsFrom) 348 modify = True 349 continue 350 351 if repsFrom.is_modified(): 352 repsFrom.set_unmodified() 353 modify = True 354 355 # current (unmodified) elements also get 356 # appended here but no changes will occur 357 # unless something is "to be modified" or 358 # "to be deleted" 359 newreps.append(ndr_pack(repsFrom.ndr_blob)) 360 361 # Now delete these from our list of rep_repsFrom 362 for repsFrom in delreps: 363 self.rep_repsFrom.remove(repsFrom) 364 delreps = [] 365 366 # Nothing to do if no reps have been modified or 367 # need to be deleted or input option has informed 368 # us to be "readonly" (ro). Leave database 369 # record "as is" 370 if not modify or ro: 371 return 372 373 m = ldb.Message() 374 m.dn = ldb.Dn(samdb, self.nc_dnstr) 375 376 m["repsFrom"] = \ 377 ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsFrom") 378 379 try: 380 samdb.modify(m) 381 382 except ldb.LdbError as estr: 383 raise KCCError("Could not set repsFrom for (%s) - (%s)" % 384 (self.nc_dnstr, estr)) 385 386 def load_replUpToDateVector(self, samdb): 387 """Given an NC replica which has been discovered thru the nTDSDSA 388 database object, load the replUpToDateVector attribute for the 389 local replica. held by my dsa. The replUpToDateVector 390 attribute is not replicated so this attribute is relative only 391 to the local DSA that the samdb exists on 392 393 """ 394 try: 395 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE, 396 attrs=["replUpToDateVector"]) 397 398 except ldb.LdbError as e2: 399 (enum, estr) = e2.args 400 raise KCCError("Unable to find NC for (%s) - (%s)" % 401 (self.nc_dnstr, estr)) 402 403 msg = res[0] 404 405 # Possibly no replUpToDateVector if this is a singleton DC 406 if "replUpToDateVector" in msg: 407 value = msg["replUpToDateVector"][0] 408 blob = ndr_unpack(drsblobs.replUpToDateVectorBlob, 409 value) 410 if blob.version != 2: 411 # Samba only generates version 2, and this runs locally 412 raise AttributeError("Unexpected replUpToDateVector version %d" 413 % blob.version) 414 415 self.rep_replUpToDateVector_cursors = blob.ctr.cursors 416 else: 417 self.rep_replUpToDateVector_cursors = [] 418 419 def dumpstr_to_be_deleted(self): 420 return '\n'.join(str(x) for x in self.rep_repsFrom if x.to_be_deleted) 421 422 def dumpstr_to_be_modified(self): 423 return '\n'.join(str(x) for x in self.rep_repsFrom if x.is_modified()) 424 425 def load_fsmo_roles(self, samdb): 426 """Given an NC replica which has been discovered thru the nTDSDSA 427 database object, load the fSMORoleOwner attribute. 428 """ 429 try: 430 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE, 431 attrs=["fSMORoleOwner"]) 432 433 except ldb.LdbError as e3: 434 (enum, estr) = e3.args 435 raise KCCError("Unable to find NC for (%s) - (%s)" % 436 (self.nc_dnstr, estr)) 437 438 msg = res[0] 439 440 # Possibly no fSMORoleOwner 441 if "fSMORoleOwner" in msg: 442 self.rep_fsmo_role_owner = msg["fSMORoleOwner"] 443 444 def is_fsmo_role_owner(self, dsa_dnstr): 445 if self.rep_fsmo_role_owner is not None and \ 446 self.rep_fsmo_role_owner == dsa_dnstr: 447 return True 448 return False 449 450 def load_repsTo(self, samdb): 451 """Given an NC replica which has been discovered thru the nTDSDSA 452 database object, load the repsTo attribute for the local replica. 453 held by my dsa. The repsTo attribute is not replicated so this 454 attribute is relative only to the local DSA that the samdb exists on 455 456 This is responsible for push replication, not scheduled pull 457 replication. Not to be confused for repsFrom. 458 """ 459 try: 460 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE, 461 attrs=["repsTo"]) 462 463 except ldb.LdbError as e4: 464 (enum, estr) = e4.args 465 raise KCCError("Unable to find NC for (%s) - (%s)" % 466 (self.nc_dnstr, estr)) 467 468 msg = res[0] 469 470 # Possibly no repsTo if this is a singleton DC 471 if "repsTo" in msg: 472 for value in msg["repsTo"]: 473 try: 474 unpacked = ndr_unpack(drsblobs.repsFromToBlob, value) 475 except RuntimeError as e: 476 print("bad repsTo NDR: %r" % (value), 477 file=sys.stderr) 478 continue 479 rep = RepsFromTo(self.nc_dnstr, unpacked) 480 self.rep_repsTo.append(rep) 481 482 def commit_repsTo(self, samdb, ro=False): 483 """Commit repsTo to the database""" 484 485 # XXX - This is not truly correct according to the MS-TECH 486 # docs. To commit a repsTo we should be using RPCs 487 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and 488 # IDL_DRSReplicaDel to affect a repsTo change. 489 # 490 # Those RPCs are missing in samba, so I'll have to 491 # implement them to get this to more accurately 492 # reflect the reference docs. As of right now this 493 # commit to the database will work as its what the 494 # older KCC also did 495 modify = False 496 newreps = [] 497 delreps = [] 498 499 for repsTo in self.rep_repsTo: 500 501 # Leave out any to be deleted from 502 # replacement list. Build a list 503 # of to be deleted reps which we will 504 # remove from rep_repsTo list below 505 if repsTo.to_be_deleted: 506 delreps.append(repsTo) 507 modify = True 508 continue 509 510 if repsTo.is_modified(): 511 repsTo.set_unmodified() 512 modify = True 513 514 # current (unmodified) elements also get 515 # appended here but no changes will occur 516 # unless something is "to be modified" or 517 # "to be deleted" 518 newreps.append(ndr_pack(repsTo.ndr_blob)) 519 520 # Now delete these from our list of rep_repsTo 521 for repsTo in delreps: 522 self.rep_repsTo.remove(repsTo) 523 delreps = [] 524 525 # Nothing to do if no reps have been modified or 526 # need to be deleted or input option has informed 527 # us to be "readonly" (ro). Leave database 528 # record "as is" 529 if not modify or ro: 530 return 531 532 m = ldb.Message() 533 m.dn = ldb.Dn(samdb, self.nc_dnstr) 534 535 m["repsTo"] = \ 536 ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsTo") 537 538 try: 539 samdb.modify(m) 540 541 except ldb.LdbError as estr: 542 raise KCCError("Could not set repsTo for (%s) - (%s)" % 543 (self.nc_dnstr, estr)) 544 545 546class DirectoryServiceAgent(object): 547 548 def __init__(self, dsa_dnstr): 549 """Initialize DSA class. 550 551 Class is subsequently fully populated by calling the load_dsa() method 552 553 :param dsa_dnstr: DN of the nTDSDSA 554 """ 555 self.dsa_dnstr = dsa_dnstr 556 self.dsa_guid = None 557 self.dsa_ivid = None 558 self.dsa_is_ro = False 559 self.dsa_is_istg = False 560 self.options = 0 561 self.dsa_behavior = 0 562 self.default_dnstr = None # default domain dn string for dsa 563 564 # NCReplicas for this dsa that are "present" 565 # Indexed by DN string of naming context 566 self.current_rep_table = {} 567 568 # NCReplicas for this dsa that "should be present" 569 # Indexed by DN string of naming context 570 self.needed_rep_table = {} 571 572 # NTDSConnections for this dsa. These are current 573 # valid connections that are committed or pending a commit 574 # in the database. Indexed by DN string of connection 575 self.connect_table = {} 576 577 def __str__(self): 578 '''Debug dump string output of class''' 579 580 text = "%s:" % self.__class__.__name__ 581 if self.dsa_dnstr is not None: 582 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr 583 if self.dsa_guid is not None: 584 text = text + "\n\tdsa_guid=%s" % str(self.dsa_guid) 585 if self.dsa_ivid is not None: 586 text = text + "\n\tdsa_ivid=%s" % str(self.dsa_ivid) 587 588 text += "\n\tro=%s" % self.is_ro() +\ 589 "\n\tgc=%s" % self.is_gc() +\ 590 "\n\tistg=%s" % self.is_istg() +\ 591 "\ncurrent_replica_table:" +\ 592 "\n%s" % self.dumpstr_current_replica_table() +\ 593 "\nneeded_replica_table:" +\ 594 "\n%s" % self.dumpstr_needed_replica_table() +\ 595 "\nconnect_table:" +\ 596 "\n%s" % self.dumpstr_connect_table() 597 598 return text 599 600 def get_current_replica(self, nc_dnstr): 601 return self.current_rep_table.get(nc_dnstr) 602 603 def is_istg(self): 604 '''Returns True if dsa is intersite topology generator for it's site''' 605 # The KCC on an RODC always acts as an ISTG for itself 606 return self.dsa_is_istg or self.dsa_is_ro 607 608 def is_ro(self): 609 '''Returns True if dsa a read only domain controller''' 610 return self.dsa_is_ro 611 612 def is_gc(self): 613 '''Returns True if dsa hosts a global catalog''' 614 if (self.options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0: 615 return True 616 return False 617 618 def is_minimum_behavior(self, version): 619 """Is dsa at minimum windows level greater than or equal to (version) 620 621 :param version: Windows version to test against 622 (e.g. DS_DOMAIN_FUNCTION_2008) 623 """ 624 if self.dsa_behavior >= version: 625 return True 626 return False 627 628 def is_translate_ntdsconn_disabled(self): 629 """Whether this allows NTDSConnection translation in its options.""" 630 if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0: 631 return True 632 return False 633 634 def get_rep_tables(self): 635 """Return DSA current and needed replica tables 636 """ 637 return self.current_rep_table, self.needed_rep_table 638 639 def get_parent_dnstr(self): 640 """Get the parent DN string of this object.""" 641 head, sep, tail = self.dsa_dnstr.partition(',') 642 return tail 643 644 def load_dsa(self, samdb): 645 """Load a DSA from the samdb. 646 647 Prior initialization has given us the DN of the DSA that we are to 648 load. This method initializes all other attributes, including loading 649 the NC replica table for this DSA. 650 """ 651 attrs = ["objectGUID", 652 "invocationID", 653 "options", 654 "msDS-isRODC", 655 "msDS-Behavior-Version"] 656 try: 657 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE, 658 attrs=attrs) 659 660 except ldb.LdbError as e5: 661 (enum, estr) = e5.args 662 raise KCCError("Unable to find nTDSDSA for (%s) - (%s)" % 663 (self.dsa_dnstr, estr)) 664 665 msg = res[0] 666 self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID", 667 msg["objectGUID"][0])) 668 669 # RODCs don't originate changes and thus have no invocationId, 670 # therefore we must check for existence first 671 if "invocationId" in msg: 672 self.dsa_ivid = misc.GUID(samdb.schema_format_value("objectGUID", 673 msg["invocationId"][0])) 674 675 if "options" in msg: 676 self.options = int(msg["options"][0]) 677 678 if "msDS-isRODC" in msg and str(msg["msDS-isRODC"][0]) == "TRUE": 679 self.dsa_is_ro = True 680 else: 681 self.dsa_is_ro = False 682 683 if "msDS-Behavior-Version" in msg: 684 self.dsa_behavior = int(msg['msDS-Behavior-Version'][0]) 685 686 # Load the NC replicas that are enumerated on this dsa 687 self.load_current_replica_table(samdb) 688 689 # Load the nTDSConnection that are enumerated on this dsa 690 self.load_connection_table(samdb) 691 692 def load_current_replica_table(self, samdb): 693 """Method to load the NC replica's listed for DSA object. 694 695 This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs, 696 hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, and 697 msDS-HasInstantiatedNCs) to determine complete list of NC replicas that 698 are enumerated for the DSA. Once a NC replica is loaded it is 699 identified (schema, config, etc) and the other replica attributes 700 (partial, ro, etc) are determined. 701 702 :param samdb: database to query for DSA replica list 703 """ 704 ncattrs = [ 705 # not RODC - default, config, schema (old style) 706 "hasMasterNCs", 707 # not RODC - default, config, schema, app NCs 708 "msDS-hasMasterNCs", 709 # domain NC partial replicas 710 "hasPartialReplicaNCs", 711 # default domain NC 712 "msDS-HasDomainNCs", 713 # RODC only - default, config, schema, app NCs 714 "msDS-hasFullReplicaNCs", 715 # Identifies if replica is coming, going, or stable 716 "msDS-HasInstantiatedNCs" 717 ] 718 try: 719 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE, 720 attrs=ncattrs) 721 722 except ldb.LdbError as e6: 723 (enum, estr) = e6.args 724 raise KCCError("Unable to find nTDSDSA NCs for (%s) - (%s)" % 725 (self.dsa_dnstr, estr)) 726 727 # The table of NCs for the dsa we are searching 728 tmp_table = {} 729 730 # We should get one response to our query here for 731 # the ntds that we requested 732 if len(res[0]) > 0: 733 734 # Our response will contain a number of elements including 735 # the dn of the dsa as well as elements for each 736 # attribute (e.g. hasMasterNCs). Each of these elements 737 # is a dictonary list which we retrieve the keys for and 738 # then iterate over them 739 for k in res[0].keys(): 740 if k == "dn": 741 continue 742 743 # For each attribute type there will be one or more DNs 744 # listed. For instance DCs normally have 3 hasMasterNCs 745 # listed. 746 for value in res[0][k]: 747 # Turn dn into a dsdb_Dn so we can use 748 # its methods to parse a binary DN 749 dsdn = dsdb_Dn(samdb, value.decode('utf8')) 750 flags = dsdn.get_binary_integer() 751 dnstr = str(dsdn.dn) 752 753 if dnstr not in tmp_table: 754 rep = NCReplica(self, dnstr) 755 tmp_table[dnstr] = rep 756 else: 757 rep = tmp_table[dnstr] 758 759 if k == "msDS-HasInstantiatedNCs": 760 rep.set_instantiated_flags(flags) 761 continue 762 763 rep.identify_by_dsa_attr(samdb, k) 764 765 # if we've identified the default domain NC 766 # then save its DN string 767 if rep.is_default(): 768 self.default_dnstr = dnstr 769 else: 770 raise KCCError("No nTDSDSA NCs for (%s)" % self.dsa_dnstr) 771 772 # Assign our newly built NC replica table to this dsa 773 self.current_rep_table = tmp_table 774 775 def add_needed_replica(self, rep): 776 """Method to add a NC replica that "should be present" to the 777 needed_rep_table. 778 """ 779 self.needed_rep_table[rep.nc_dnstr] = rep 780 781 def load_connection_table(self, samdb): 782 """Method to load the nTDSConnections listed for DSA object. 783 784 :param samdb: database to query for DSA connection list 785 """ 786 try: 787 res = samdb.search(base=self.dsa_dnstr, 788 scope=ldb.SCOPE_SUBTREE, 789 expression="(objectClass=nTDSConnection)") 790 791 except ldb.LdbError as e7: 792 (enum, estr) = e7.args 793 raise KCCError("Unable to find nTDSConnection for (%s) - (%s)" % 794 (self.dsa_dnstr, estr)) 795 796 for msg in res: 797 dnstr = str(msg.dn) 798 799 # already loaded 800 if dnstr in self.connect_table: 801 continue 802 803 connect = NTDSConnection(dnstr) 804 805 connect.load_connection(samdb) 806 self.connect_table[dnstr] = connect 807 808 def commit_connections(self, samdb, ro=False): 809 """Method to commit any uncommitted nTDSConnections 810 modifications that are in our table. These would be 811 identified connections that are marked to be added or 812 deleted 813 814 :param samdb: database to commit DSA connection list to 815 :param ro: if (true) then peform internal operations but 816 do not write to the database (readonly) 817 """ 818 delconn = [] 819 820 for dnstr, connect in self.connect_table.items(): 821 if connect.to_be_added: 822 connect.commit_added(samdb, ro) 823 824 if connect.to_be_modified: 825 connect.commit_modified(samdb, ro) 826 827 if connect.to_be_deleted: 828 connect.commit_deleted(samdb, ro) 829 delconn.append(dnstr) 830 831 # Now delete the connection from the table 832 for dnstr in delconn: 833 del self.connect_table[dnstr] 834 835 def add_connection(self, dnstr, connect): 836 assert dnstr not in self.connect_table 837 self.connect_table[dnstr] = connect 838 839 def get_connection_by_from_dnstr(self, from_dnstr): 840 """Scan DSA nTDSConnection table and return connection 841 with a "fromServer" dn string equivalent to method 842 input parameter. 843 844 :param from_dnstr: search for this from server entry 845 """ 846 answer = [] 847 for connect in self.connect_table.values(): 848 if connect.get_from_dnstr() == from_dnstr: 849 answer.append(connect) 850 851 return answer 852 853 def dumpstr_current_replica_table(self): 854 '''Debug dump string output of current replica table''' 855 return '\n'.join(str(x) for x in self.current_rep_table) 856 857 def dumpstr_needed_replica_table(self): 858 '''Debug dump string output of needed replica table''' 859 return '\n'.join(str(x) for x in self.needed_rep_table) 860 861 def dumpstr_connect_table(self): 862 '''Debug dump string output of connect table''' 863 return '\n'.join(str(x) for x in self.connect_table) 864 865 def new_connection(self, options, system_flags, transport, from_dnstr, 866 sched): 867 """Set up a new connection for the DSA based on input 868 parameters. Connection will be added to the DSA 869 connect_table and will be marked as "to be added" pending 870 a call to commit_connections() 871 """ 872 dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr 873 874 connect = NTDSConnection(dnstr) 875 connect.to_be_added = True 876 connect.enabled = True 877 connect.from_dnstr = from_dnstr 878 connect.options = options 879 connect.system_flags = system_flags 880 881 if transport is not None: 882 connect.transport_dnstr = transport.dnstr 883 connect.transport_guid = transport.guid 884 885 if sched is not None: 886 connect.schedule = sched 887 else: 888 # Create schedule. Attribute valuse set according to MS-TECH 889 # intrasite connection creation document 890 connect.schedule = new_connection_schedule() 891 892 self.add_connection(dnstr, connect) 893 return connect 894 895 896class NTDSConnection(object): 897 """Class defines a nTDSConnection found under a DSA 898 """ 899 def __init__(self, dnstr): 900 self.dnstr = dnstr 901 self.guid = None 902 self.enabled = False 903 self.whenCreated = 0 904 self.to_be_added = False # new connection needs to be added 905 self.to_be_deleted = False # old connection needs to be deleted 906 self.to_be_modified = False 907 self.options = 0 908 self.system_flags = 0 909 self.transport_dnstr = None 910 self.transport_guid = None 911 self.from_dnstr = None 912 self.schedule = None 913 914 def __str__(self): 915 '''Debug dump string output of NTDSConnection object''' 916 917 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr) +\ 918 "\n\tenabled=%s" % self.enabled +\ 919 "\n\tto_be_added=%s" % self.to_be_added +\ 920 "\n\tto_be_deleted=%s" % self.to_be_deleted +\ 921 "\n\tto_be_modified=%s" % self.to_be_modified +\ 922 "\n\toptions=0x%08X" % self.options +\ 923 "\n\tsystem_flags=0x%08X" % self.system_flags +\ 924 "\n\twhenCreated=%d" % self.whenCreated +\ 925 "\n\ttransport_dn=%s" % self.transport_dnstr 926 927 if self.guid is not None: 928 text += "\n\tguid=%s" % str(self.guid) 929 930 if self.transport_guid is not None: 931 text += "\n\ttransport_guid=%s" % str(self.transport_guid) 932 933 text = text + "\n\tfrom_dn=%s" % self.from_dnstr 934 935 if self.schedule is not None: 936 text += "\n\tschedule.size=%s" % self.schedule.size +\ 937 "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth +\ 938 ("\n\tschedule.numberOfSchedules=%s" % 939 self.schedule.numberOfSchedules) 940 941 for i, header in enumerate(self.schedule.headerArray): 942 text += ("\n\tschedule.headerArray[%d].type=%d" % 943 (i, header.type)) +\ 944 ("\n\tschedule.headerArray[%d].offset=%d" % 945 (i, header.offset)) +\ 946 "\n\tschedule.dataArray[%d].slots[ " % i +\ 947 "".join("0x%X " % slot for slot in self.schedule.dataArray[i].slots) +\ 948 "]" 949 950 return text 951 952 def load_connection(self, samdb): 953 """Given a NTDSConnection object with an prior initialization 954 for the object's DN, search for the DN and load attributes 955 from the samdb. 956 """ 957 attrs = ["options", 958 "enabledConnection", 959 "schedule", 960 "whenCreated", 961 "objectGUID", 962 "transportType", 963 "fromServer", 964 "systemFlags"] 965 try: 966 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE, 967 attrs=attrs) 968 969 except ldb.LdbError as e8: 970 (enum, estr) = e8.args 971 raise KCCError("Unable to find nTDSConnection for (%s) - (%s)" % 972 (self.dnstr, estr)) 973 974 msg = res[0] 975 976 if "options" in msg: 977 self.options = int(msg["options"][0]) 978 979 if "enabledConnection" in msg: 980 if str(msg["enabledConnection"][0]).upper().lstrip().rstrip() == "TRUE": 981 self.enabled = True 982 983 if "systemFlags" in msg: 984 self.system_flags = int(msg["systemFlags"][0]) 985 986 try: 987 self.guid = \ 988 misc.GUID(samdb.schema_format_value("objectGUID", 989 msg["objectGUID"][0])) 990 except KeyError: 991 raise KCCError("Unable to find objectGUID in nTDSConnection " 992 "for (%s)" % (self.dnstr)) 993 994 if "transportType" in msg: 995 dsdn = dsdb_Dn(samdb, msg["transportType"][0].decode('utf8')) 996 self.load_connection_transport(samdb, str(dsdn.dn)) 997 998 if "schedule" in msg: 999 self.schedule = ndr_unpack(drsblobs.schedule, msg["schedule"][0]) 1000 1001 if "whenCreated" in msg: 1002 self.whenCreated = ldb.string_to_time(str(msg["whenCreated"][0])) 1003 1004 if "fromServer" in msg: 1005 dsdn = dsdb_Dn(samdb, msg["fromServer"][0].decode('utf8')) 1006 self.from_dnstr = str(dsdn.dn) 1007 assert self.from_dnstr is not None 1008 1009 def load_connection_transport(self, samdb, tdnstr): 1010 """Given a NTDSConnection object which enumerates a transport 1011 DN, load the transport information for the connection object 1012 1013 :param tdnstr: transport DN to load 1014 """ 1015 attrs = ["objectGUID"] 1016 try: 1017 res = samdb.search(base=tdnstr, 1018 scope=ldb.SCOPE_BASE, attrs=attrs) 1019 1020 except ldb.LdbError as e9: 1021 (enum, estr) = e9.args 1022 raise KCCError("Unable to find transport (%s) - (%s)" % 1023 (tdnstr, estr)) 1024 1025 if "objectGUID" in res[0]: 1026 msg = res[0] 1027 self.transport_dnstr = tdnstr 1028 self.transport_guid = \ 1029 misc.GUID(samdb.schema_format_value("objectGUID", 1030 msg["objectGUID"][0])) 1031 assert self.transport_dnstr is not None 1032 assert self.transport_guid is not None 1033 1034 def commit_deleted(self, samdb, ro=False): 1035 """Local helper routine for commit_connections() which 1036 handles committed connections that are to be deleted from 1037 the database database 1038 """ 1039 assert self.to_be_deleted 1040 self.to_be_deleted = False 1041 1042 # No database modification requested 1043 if ro: 1044 return 1045 1046 try: 1047 samdb.delete(self.dnstr) 1048 except ldb.LdbError as e10: 1049 (enum, estr) = e10.args 1050 raise KCCError("Could not delete nTDSConnection for (%s) - (%s)" % 1051 (self.dnstr, estr)) 1052 1053 def commit_added(self, samdb, ro=False): 1054 """Local helper routine for commit_connections() which 1055 handles committed connections that are to be added to the 1056 database 1057 """ 1058 assert self.to_be_added 1059 self.to_be_added = False 1060 1061 # No database modification requested 1062 if ro: 1063 return 1064 1065 # First verify we don't have this entry to ensure nothing 1066 # is programatically amiss 1067 found = False 1068 try: 1069 msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE) 1070 if len(msg) != 0: 1071 found = True 1072 1073 except ldb.LdbError as e11: 1074 (enum, estr) = e11.args 1075 if enum != ldb.ERR_NO_SUCH_OBJECT: 1076 raise KCCError("Unable to search for (%s) - (%s)" % 1077 (self.dnstr, estr)) 1078 if found: 1079 raise KCCError("nTDSConnection for (%s) already exists!" % 1080 self.dnstr) 1081 1082 if self.enabled: 1083 enablestr = "TRUE" 1084 else: 1085 enablestr = "FALSE" 1086 1087 # Prepare a message for adding to the samdb 1088 m = ldb.Message() 1089 m.dn = ldb.Dn(samdb, self.dnstr) 1090 1091 m["objectClass"] = \ 1092 ldb.MessageElement("nTDSConnection", ldb.FLAG_MOD_ADD, 1093 "objectClass") 1094 m["showInAdvancedViewOnly"] = \ 1095 ldb.MessageElement("TRUE", ldb.FLAG_MOD_ADD, 1096 "showInAdvancedViewOnly") 1097 m["enabledConnection"] = \ 1098 ldb.MessageElement(enablestr, ldb.FLAG_MOD_ADD, 1099 "enabledConnection") 1100 m["fromServer"] = \ 1101 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_ADD, "fromServer") 1102 m["options"] = \ 1103 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options") 1104 m["systemFlags"] = \ 1105 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_ADD, 1106 "systemFlags") 1107 1108 if self.transport_dnstr is not None: 1109 m["transportType"] = \ 1110 ldb.MessageElement(str(self.transport_dnstr), ldb.FLAG_MOD_ADD, 1111 "transportType") 1112 1113 if self.schedule is not None: 1114 m["schedule"] = \ 1115 ldb.MessageElement(ndr_pack(self.schedule), 1116 ldb.FLAG_MOD_ADD, "schedule") 1117 try: 1118 samdb.add(m) 1119 except ldb.LdbError as e12: 1120 (enum, estr) = e12.args 1121 raise KCCError("Could not add nTDSConnection for (%s) - (%s)" % 1122 (self.dnstr, estr)) 1123 1124 def commit_modified(self, samdb, ro=False): 1125 """Local helper routine for commit_connections() which 1126 handles committed connections that are to be modified to the 1127 database 1128 """ 1129 assert self.to_be_modified 1130 self.to_be_modified = False 1131 1132 # No database modification requested 1133 if ro: 1134 return 1135 1136 # First verify we have this entry to ensure nothing 1137 # is programatically amiss 1138 try: 1139 # we don't use the search result, but it tests the status 1140 # of self.dnstr in the database. 1141 samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE) 1142 1143 except ldb.LdbError as e13: 1144 (enum, estr) = e13.args 1145 if enum == ldb.ERR_NO_SUCH_OBJECT: 1146 raise KCCError("nTDSConnection for (%s) doesn't exist!" % 1147 self.dnstr) 1148 raise KCCError("Unable to search for (%s) - (%s)" % 1149 (self.dnstr, estr)) 1150 1151 if self.enabled: 1152 enablestr = "TRUE" 1153 else: 1154 enablestr = "FALSE" 1155 1156 # Prepare a message for modifying the samdb 1157 m = ldb.Message() 1158 m.dn = ldb.Dn(samdb, self.dnstr) 1159 1160 m["enabledConnection"] = \ 1161 ldb.MessageElement(enablestr, ldb.FLAG_MOD_REPLACE, 1162 "enabledConnection") 1163 m["fromServer"] = \ 1164 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_REPLACE, 1165 "fromServer") 1166 m["options"] = \ 1167 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_REPLACE, 1168 "options") 1169 m["systemFlags"] = \ 1170 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_REPLACE, 1171 "systemFlags") 1172 1173 if self.transport_dnstr is not None: 1174 m["transportType"] = \ 1175 ldb.MessageElement(str(self.transport_dnstr), 1176 ldb.FLAG_MOD_REPLACE, "transportType") 1177 else: 1178 m["transportType"] = \ 1179 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "transportType") 1180 1181 if self.schedule is not None: 1182 m["schedule"] = \ 1183 ldb.MessageElement(ndr_pack(self.schedule), 1184 ldb.FLAG_MOD_REPLACE, "schedule") 1185 else: 1186 m["schedule"] = \ 1187 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "schedule") 1188 try: 1189 samdb.modify(m) 1190 except ldb.LdbError as e14: 1191 (enum, estr) = e14.args 1192 raise KCCError("Could not modify nTDSConnection for (%s) - (%s)" % 1193 (self.dnstr, estr)) 1194 1195 def set_modified(self, truefalse): 1196 self.to_be_modified = truefalse 1197 1198 def is_schedule_minimum_once_per_week(self): 1199 """Returns True if our schedule includes at least one 1200 replication interval within the week. False otherwise 1201 """ 1202 # replinfo schedule is None means "always", while 1203 # NTDSConnection schedule is None means "never". 1204 if self.schedule is None or self.schedule.dataArray[0] is None: 1205 return False 1206 1207 for slot in self.schedule.dataArray[0].slots: 1208 if (slot & 0x0F) != 0x0: 1209 return True 1210 return False 1211 1212 def is_equivalent_schedule(self, sched): 1213 """Returns True if our schedule is equivalent to the input 1214 comparison schedule. 1215 1216 :param shed: schedule to compare to 1217 """ 1218 # There are 4 cases, where either self.schedule or sched can be None 1219 # 1220 # | self. is None | self. is not None 1221 # --------------+-----------------+-------------------- 1222 # sched is None | True | False 1223 # --------------+-----------------+-------------------- 1224 # sched is not None | False | do calculations 1225 1226 if self.schedule is None: 1227 return sched is None 1228 1229 if sched is None: 1230 return False 1231 1232 if ((self.schedule.size != sched.size or 1233 self.schedule.bandwidth != sched.bandwidth or 1234 self.schedule.numberOfSchedules != sched.numberOfSchedules)): 1235 return False 1236 1237 for i, header in enumerate(self.schedule.headerArray): 1238 1239 if self.schedule.headerArray[i].type != sched.headerArray[i].type: 1240 return False 1241 1242 if self.schedule.headerArray[i].offset != \ 1243 sched.headerArray[i].offset: 1244 return False 1245 1246 for a, b in zip(self.schedule.dataArray[i].slots, 1247 sched.dataArray[i].slots): 1248 if a != b: 1249 return False 1250 return True 1251 1252 def is_rodc_topology(self): 1253 """Returns True if NTDS Connection specifies RODC 1254 topology only 1255 """ 1256 if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0: 1257 return False 1258 return True 1259 1260 def is_generated(self): 1261 """Returns True if NTDS Connection was generated by the 1262 KCC topology algorithm as opposed to set by the administrator 1263 """ 1264 if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0: 1265 return False 1266 return True 1267 1268 def is_override_notify_default(self): 1269 """Returns True if NTDS Connection should override notify default 1270 """ 1271 if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0: 1272 return False 1273 return True 1274 1275 def is_use_notify(self): 1276 """Returns True if NTDS Connection should use notify 1277 """ 1278 if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0: 1279 return False 1280 return True 1281 1282 def is_twoway_sync(self): 1283 """Returns True if NTDS Connection should use twoway sync 1284 """ 1285 if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0: 1286 return False 1287 return True 1288 1289 def is_intersite_compression_disabled(self): 1290 """Returns True if NTDS Connection intersite compression 1291 is disabled 1292 """ 1293 if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0: 1294 return False 1295 return True 1296 1297 def is_user_owned_schedule(self): 1298 """Returns True if NTDS Connection has a user owned schedule 1299 """ 1300 if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0: 1301 return False 1302 return True 1303 1304 def is_enabled(self): 1305 """Returns True if NTDS Connection is enabled 1306 """ 1307 return self.enabled 1308 1309 def get_from_dnstr(self): 1310 '''Return fromServer dn string attribute''' 1311 return self.from_dnstr 1312 1313 1314class Partition(NamingContext): 1315 """A naming context discovered thru Partitions DN of the config schema. 1316 1317 This is a more specific form of NamingContext class (inheriting from that 1318 class) and it identifies unique attributes enumerated in the Partitions 1319 such as which nTDSDSAs are cross referenced for replicas 1320 """ 1321 def __init__(self, partstr): 1322 self.partstr = partstr 1323 self.enabled = True 1324 self.system_flags = 0 1325 self.rw_location_list = [] 1326 self.ro_location_list = [] 1327 1328 # We don't have enough info to properly 1329 # fill in the naming context yet. We'll get that 1330 # fully set up with load_partition(). 1331 NamingContext.__init__(self, None) 1332 1333 def load_partition(self, samdb): 1334 """Given a Partition class object that has been initialized with its 1335 partition dn string, load the partition from the sam database, identify 1336 the type of the partition (schema, domain, etc) and record the list of 1337 nTDSDSAs that appear in the cross reference attributes 1338 msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations. 1339 1340 :param samdb: sam database to load partition from 1341 """ 1342 attrs = ["nCName", 1343 "Enabled", 1344 "systemFlags", 1345 "msDS-NC-Replica-Locations", 1346 "msDS-NC-RO-Replica-Locations"] 1347 try: 1348 res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE, 1349 attrs=attrs) 1350 1351 except ldb.LdbError as e15: 1352 (enum, estr) = e15.args 1353 raise KCCError("Unable to find partition for (%s) - (%s)" % 1354 (self.partstr, estr)) 1355 msg = res[0] 1356 for k in msg.keys(): 1357 if k == "dn": 1358 continue 1359 1360 if k == "Enabled": 1361 if str(msg[k][0]).upper().lstrip().rstrip() == "TRUE": 1362 self.enabled = True 1363 else: 1364 self.enabled = False 1365 continue 1366 1367 if k == "systemFlags": 1368 self.system_flags = int(msg[k][0]) 1369 continue 1370 1371 for value in msg[k]: 1372 dsdn = dsdb_Dn(samdb, value.decode('utf8')) 1373 dnstr = str(dsdn.dn) 1374 1375 if k == "nCName": 1376 self.nc_dnstr = dnstr 1377 continue 1378 1379 if k == "msDS-NC-Replica-Locations": 1380 self.rw_location_list.append(dnstr) 1381 continue 1382 1383 if k == "msDS-NC-RO-Replica-Locations": 1384 self.ro_location_list.append(dnstr) 1385 continue 1386 1387 # Now identify what type of NC this partition 1388 # enumerated 1389 self.identify_by_basedn(samdb) 1390 1391 def is_enabled(self): 1392 """Returns True if partition is enabled 1393 """ 1394 return self.is_enabled 1395 1396 def is_foreign(self): 1397 """Returns True if this is not an Active Directory NC in our 1398 forest but is instead something else (e.g. a foreign NC) 1399 """ 1400 if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0: 1401 return True 1402 else: 1403 return False 1404 1405 def should_be_present(self, target_dsa): 1406 """Tests whether this partition should have an NC replica 1407 on the target dsa. This method returns a tuple of 1408 needed=True/False, ro=True/False, partial=True/False 1409 1410 :param target_dsa: should NC be present on target dsa 1411 """ 1412 ro = False 1413 partial = False 1414 1415 # If this is the config, schema, or default 1416 # domain NC for the target dsa then it should 1417 # be present 1418 needed = (self.nc_type == NCType.config or 1419 self.nc_type == NCType.schema or 1420 (self.nc_type == NCType.domain and 1421 self.nc_dnstr == target_dsa.default_dnstr)) 1422 1423 # A writable replica of an application NC should be present 1424 # if there a cross reference to the target DSA exists. Depending 1425 # on whether the DSA is ro we examine which type of cross reference 1426 # to look for (msDS-NC-Replica-Locations or 1427 # msDS-NC-RO-Replica-Locations 1428 if self.nc_type == NCType.application: 1429 if target_dsa.is_ro(): 1430 if target_dsa.dsa_dnstr in self.ro_location_list: 1431 needed = True 1432 else: 1433 if target_dsa.dsa_dnstr in self.rw_location_list: 1434 needed = True 1435 1436 # If the target dsa is a gc then a partial replica of a 1437 # domain NC (other than the DSAs default domain) should exist 1438 # if there is also a cross reference for the DSA 1439 if (target_dsa.is_gc() and 1440 self.nc_type == NCType.domain and 1441 self.nc_dnstr != target_dsa.default_dnstr and 1442 (target_dsa.dsa_dnstr in self.ro_location_list or 1443 target_dsa.dsa_dnstr in self.rw_location_list)): 1444 needed = True 1445 partial = True 1446 1447 # partial NCs are always readonly 1448 if needed and (target_dsa.is_ro() or partial): 1449 ro = True 1450 1451 return needed, ro, partial 1452 1453 def __str__(self): 1454 '''Debug dump string output of class''' 1455 text = "%s" % NamingContext.__str__(self) +\ 1456 "\n\tpartdn=%s" % self.partstr +\ 1457 "".join("\n\tmsDS-NC-Replica-Locations=%s" % k for k in self.rw_location_list) +\ 1458 "".join("\n\tmsDS-NC-RO-Replica-Locations=%s" % k for k in self.ro_location_list) 1459 return text 1460 1461 1462class Site(object): 1463 """An individual site object discovered thru the configuration 1464 naming context. Contains all DSAs that exist within the site 1465 """ 1466 def __init__(self, site_dnstr, nt_now): 1467 self.site_dnstr = site_dnstr 1468 self.site_guid = None 1469 self.site_options = 0 1470 self.site_topo_generator = None 1471 self.site_topo_failover = 0 # appears to be in minutes 1472 self.dsa_table = {} 1473 self.rw_dsa_table = {} 1474 self.nt_now = nt_now 1475 1476 def load_site(self, samdb): 1477 """Loads the NTDS Site Settings options attribute for the site 1478 as well as querying and loading all DSAs that appear within 1479 the site. 1480 """ 1481 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr 1482 attrs = ["options", 1483 "interSiteTopologyFailover", 1484 "interSiteTopologyGenerator"] 1485 try: 1486 res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE, 1487 attrs=attrs) 1488 self_res = samdb.search(base=self.site_dnstr, scope=ldb.SCOPE_BASE, 1489 attrs=['objectGUID']) 1490 except ldb.LdbError as e16: 1491 (enum, estr) = e16.args 1492 raise KCCError("Unable to find site settings for (%s) - (%s)" % 1493 (ssdn, estr)) 1494 1495 msg = res[0] 1496 if "options" in msg: 1497 self.site_options = int(msg["options"][0]) 1498 1499 if "interSiteTopologyGenerator" in msg: 1500 self.site_topo_generator = \ 1501 str(msg["interSiteTopologyGenerator"][0]) 1502 1503 if "interSiteTopologyFailover" in msg: 1504 self.site_topo_failover = int(msg["interSiteTopologyFailover"][0]) 1505 1506 msg = self_res[0] 1507 if "objectGUID" in msg: 1508 self.site_guid = misc.GUID(samdb.schema_format_value("objectGUID", 1509 msg["objectGUID"][0])) 1510 1511 self.load_all_dsa(samdb) 1512 1513 def load_all_dsa(self, samdb): 1514 """Discover all nTDSDSA thru the sites entry and 1515 instantiate and load the DSAs. Each dsa is inserted 1516 into the dsa_table by dn string. 1517 """ 1518 try: 1519 res = samdb.search(self.site_dnstr, 1520 scope=ldb.SCOPE_SUBTREE, 1521 expression="(objectClass=nTDSDSA)") 1522 except ldb.LdbError as e17: 1523 (enum, estr) = e17.args 1524 raise KCCError("Unable to find nTDSDSAs - (%s)" % estr) 1525 1526 for msg in res: 1527 dnstr = str(msg.dn) 1528 1529 # already loaded 1530 if dnstr in self.dsa_table: 1531 continue 1532 1533 dsa = DirectoryServiceAgent(dnstr) 1534 1535 dsa.load_dsa(samdb) 1536 1537 # Assign this dsa to my dsa table 1538 # and index by dsa dn 1539 self.dsa_table[dnstr] = dsa 1540 if not dsa.is_ro(): 1541 self.rw_dsa_table[dnstr] = dsa 1542 1543 def get_dsa(self, dnstr): 1544 """Return a previously loaded DSA object by consulting 1545 the sites dsa_table for the provided DSA dn string 1546 1547 :return: None if DSA doesn't exist 1548 """ 1549 return self.dsa_table.get(dnstr) 1550 1551 def select_istg(self, samdb, mydsa, ro): 1552 """Determine if my DC should be an intersite topology 1553 generator. If my DC is the istg and is both a writeable 1554 DC and the database is opened in write mode then we perform 1555 an originating update to set the interSiteTopologyGenerator 1556 attribute in the NTDS Site Settings object. An RODC always 1557 acts as an ISTG for itself. 1558 """ 1559 # The KCC on an RODC always acts as an ISTG for itself 1560 if mydsa.dsa_is_ro: 1561 mydsa.dsa_is_istg = True 1562 self.site_topo_generator = mydsa.dsa_dnstr 1563 return True 1564 1565 c_rep = get_dsa_config_rep(mydsa) 1566 1567 # Load repsFrom and replUpToDateVector if not already loaded 1568 # so we can get the current state of the config replica and 1569 # whether we are getting updates from the istg 1570 c_rep.load_repsFrom(samdb) 1571 1572 c_rep.load_replUpToDateVector(samdb) 1573 1574 # From MS-ADTS 6.2.2.3.1 ISTG selection: 1575 # First, the KCC on a writable DC determines whether it acts 1576 # as an ISTG for its site 1577 # 1578 # Let s be the object such that s!lDAPDisplayName = nTDSDSA 1579 # and classSchema in s!objectClass. 1580 # 1581 # Let D be the sequence of objects o in the site of the local 1582 # DC such that o!objectCategory = s. D is sorted in ascending 1583 # order by objectGUID. 1584 # 1585 # Which is a fancy way of saying "sort all the nTDSDSA objects 1586 # in the site by guid in ascending order". Place sorted list 1587 # in D_sort[] 1588 D_sort = sorted( 1589 self.rw_dsa_table.values(), 1590 key=lambda dsa: ndr_pack(dsa.dsa_guid)) 1591 1592 # double word number of 100 nanosecond intervals since 1600s 1593 1594 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours 1595 # if o!interSiteTopologyFailover is 0 or has no value. 1596 # 1597 # Note: lastSuccess and ntnow are in 100 nanosecond intervals 1598 # so it appears we have to turn f into the same interval 1599 # 1600 # interSiteTopologyFailover (if set) appears to be in minutes 1601 # so we'll need to convert to senconds and then 100 nanosecond 1602 # intervals 1603 # XXX [MS-ADTS] 6.2.2.3.1 says it is seconds, not minutes. 1604 # 1605 # 10,000,000 is number of 100 nanosecond intervals in a second 1606 if self.site_topo_failover == 0: 1607 f = 2 * 60 * 60 * 10000000 1608 else: 1609 f = self.site_topo_failover * 60 * 10000000 1610 1611 # Let o be the site settings object for the site of the local 1612 # DC, or NULL if no such o exists. 1613 d_dsa = self.dsa_table.get(self.site_topo_generator) 1614 1615 # From MS-ADTS 6.2.2.3.1 ISTG selection: 1616 # If o != NULL and o!interSiteTopologyGenerator is not the 1617 # nTDSDSA object for the local DC and 1618 # o!interSiteTopologyGenerator is an element dj of sequence D: 1619 # 1620 if d_dsa is not None and d_dsa is not mydsa: 1621 # From MS-ADTS 6.2.2.3.1 ISTG Selection: 1622 # Let c be the cursor in the replUpToDateVector variable 1623 # associated with the NC replica of the config NC such 1624 # that c.uuidDsa = dj!invocationId. If no such c exists 1625 # (No evidence of replication from current ITSG): 1626 # Let i = j. 1627 # Let t = 0. 1628 # 1629 # Else if the current time < c.timeLastSyncSuccess - f 1630 # (Evidence of time sync problem on current ISTG): 1631 # Let i = 0. 1632 # Let t = 0. 1633 # 1634 # Else (Evidence of replication from current ITSG): 1635 # Let i = j. 1636 # Let t = c.timeLastSyncSuccess. 1637 # 1638 # last_success appears to be a double word containing 1639 # number of 100 nanosecond intervals since the 1600s 1640 j_idx = D_sort.index(d_dsa) 1641 1642 found = False 1643 for cursor in c_rep.rep_replUpToDateVector_cursors: 1644 if d_dsa.dsa_ivid == cursor.source_dsa_invocation_id: 1645 found = True 1646 break 1647 1648 if not found: 1649 i_idx = j_idx 1650 t_time = 0 1651 1652 # XXX doc says current time < c.timeLastSyncSuccess - f 1653 # which is true only if f is negative or clocks are wrong. 1654 # f is not negative in the default case (2 hours). 1655 elif self.nt_now - cursor.last_sync_success > f: 1656 i_idx = 0 1657 t_time = 0 1658 else: 1659 i_idx = j_idx 1660 t_time = cursor.last_sync_success 1661 1662 # Otherwise (Nominate local DC as ISTG): 1663 # Let i be the integer such that di is the nTDSDSA 1664 # object for the local DC. 1665 # Let t = the current time. 1666 else: 1667 i_idx = D_sort.index(mydsa) 1668 t_time = self.nt_now 1669 1670 # Compute a function that maintains the current ISTG if 1671 # it is alive, cycles through other candidates if not. 1672 # 1673 # Let k be the integer (i + ((current time - t) / 1674 # o!interSiteTopologyFailover)) MOD |D|. 1675 # 1676 # Note: We don't want to divide by zero here so they must 1677 # have meant "f" instead of "o!interSiteTopologyFailover" 1678 k_idx = (i_idx + ((self.nt_now - t_time) // f)) % len(D_sort) 1679 1680 # The local writable DC acts as an ISTG for its site if and 1681 # only if dk is the nTDSDSA object for the local DC. If the 1682 # local DC does not act as an ISTG, the KCC skips the 1683 # remainder of this task. 1684 d_dsa = D_sort[k_idx] 1685 d_dsa.dsa_is_istg = True 1686 1687 # Update if we are the ISTG, otherwise return 1688 if d_dsa is not mydsa: 1689 return False 1690 1691 # Nothing to do 1692 if self.site_topo_generator == mydsa.dsa_dnstr: 1693 return True 1694 1695 self.site_topo_generator = mydsa.dsa_dnstr 1696 1697 # If readonly database then do not perform a 1698 # persistent update 1699 if ro: 1700 return True 1701 1702 # Perform update to the samdb 1703 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr 1704 1705 m = ldb.Message() 1706 m.dn = ldb.Dn(samdb, ssdn) 1707 1708 m["interSiteTopologyGenerator"] = \ 1709 ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE, 1710 "interSiteTopologyGenerator") 1711 try: 1712 samdb.modify(m) 1713 1714 except ldb.LdbError as estr: 1715 raise KCCError( 1716 "Could not set interSiteTopologyGenerator for (%s) - (%s)" % 1717 (ssdn, estr)) 1718 return True 1719 1720 def is_intrasite_topology_disabled(self): 1721 '''Returns True if intra-site topology is disabled for site''' 1722 return (self.site_options & 1723 dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0 1724 1725 def is_intersite_topology_disabled(self): 1726 '''Returns True if inter-site topology is disabled for site''' 1727 return ((self.site_options & 1728 dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED) 1729 != 0) 1730 1731 def is_random_bridgehead_disabled(self): 1732 '''Returns True if selection of random bridgehead is disabled''' 1733 return (self.site_options & 1734 dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0 1735 1736 def is_detect_stale_disabled(self): 1737 '''Returns True if detect stale is disabled for site''' 1738 return (self.site_options & 1739 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0 1740 1741 def is_cleanup_ntdsconn_disabled(self): 1742 '''Returns True if NTDS Connection cleanup is disabled for site''' 1743 return (self.site_options & 1744 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0 1745 1746 def same_site(self, dsa): 1747 '''Return True if dsa is in this site''' 1748 if self.get_dsa(dsa.dsa_dnstr): 1749 return True 1750 return False 1751 1752 def is_rodc_site(self): 1753 if len(self.dsa_table) > 0 and len(self.rw_dsa_table) == 0: 1754 return True 1755 return False 1756 1757 def __str__(self): 1758 '''Debug dump string output of class''' 1759 text = "%s:" % self.__class__.__name__ +\ 1760 "\n\tdn=%s" % self.site_dnstr +\ 1761 "\n\toptions=0x%X" % self.site_options +\ 1762 "\n\ttopo_generator=%s" % self.site_topo_generator +\ 1763 "\n\ttopo_failover=%d" % self.site_topo_failover 1764 for key, dsa in self.dsa_table.items(): 1765 text = text + "\n%s" % dsa 1766 return text 1767 1768 1769class GraphNode(object): 1770 """A graph node describing a set of edges that should be directed to it. 1771 1772 Each edge is a connection for a particular naming context replica directed 1773 from another node in the forest to this node. 1774 """ 1775 1776 def __init__(self, dsa_dnstr, max_node_edges): 1777 """Instantiate the graph node according to a DSA dn string 1778 1779 :param max_node_edges: maximum number of edges that should ever 1780 be directed to the node 1781 """ 1782 self.max_edges = max_node_edges 1783 self.dsa_dnstr = dsa_dnstr 1784 self.edge_from = [] 1785 1786 def __str__(self): 1787 text = "%s:" % self.__class__.__name__ +\ 1788 "\n\tdsa_dnstr=%s" % self.dsa_dnstr +\ 1789 "\n\tmax_edges=%d" % self.max_edges 1790 1791 for i, edge in enumerate(self.edge_from): 1792 if isinstance(edge, str): 1793 text += "\n\tedge_from[%d]=%s" % (i, edge) 1794 1795 return text 1796 1797 def add_edge_from(self, from_dsa_dnstr): 1798 """Add an edge from the dsa to our graph nodes edge from list 1799 1800 :param from_dsa_dnstr: the dsa that the edge emanates from 1801 """ 1802 assert isinstance(from_dsa_dnstr, str) 1803 1804 # No edges from myself to myself 1805 if from_dsa_dnstr == self.dsa_dnstr: 1806 return False 1807 # Only one edge from a particular node 1808 if from_dsa_dnstr in self.edge_from: 1809 return False 1810 # Not too many edges 1811 if len(self.edge_from) >= self.max_edges: 1812 return False 1813 self.edge_from.append(from_dsa_dnstr) 1814 return True 1815 1816 def add_edges_from_connections(self, dsa): 1817 """For each nTDSConnection object associated with a particular 1818 DSA, we test if it implies an edge to this graph node (i.e. 1819 the "fromServer" attribute). If it does then we add an 1820 edge from the server unless we are over the max edges for this 1821 graph node 1822 1823 :param dsa: dsa with a dnstr equivalent to his graph node 1824 """ 1825 for connect in dsa.connect_table.values(): 1826 self.add_edge_from(connect.from_dnstr) 1827 1828 def add_connections_from_edges(self, dsa, transport): 1829 """For each edge directed to this graph node, ensure there 1830 is a corresponding nTDSConnection object in the dsa. 1831 """ 1832 for edge_dnstr in self.edge_from: 1833 connections = dsa.get_connection_by_from_dnstr(edge_dnstr) 1834 1835 # For each edge directed to the NC replica that 1836 # "should be present" on the local DC, the KCC determines 1837 # whether an object c exists such that: 1838 # 1839 # c is a child of the DC's nTDSDSA object. 1840 # c.objectCategory = nTDSConnection 1841 # 1842 # Given the NC replica ri from which the edge is directed, 1843 # c.fromServer is the dsname of the nTDSDSA object of 1844 # the DC on which ri "is present". 1845 # 1846 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY 1847 1848 found_valid = False 1849 for connect in connections: 1850 if connect.is_rodc_topology(): 1851 continue 1852 found_valid = True 1853 1854 if found_valid: 1855 continue 1856 1857 # if no such object exists then the KCC adds an object 1858 # c with the following attributes 1859 1860 # Generate a new dnstr for this nTDSConnection 1861 opt = dsdb.NTDSCONN_OPT_IS_GENERATED 1862 flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME | 1863 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE) 1864 1865 dsa.new_connection(opt, flags, transport, edge_dnstr, None) 1866 1867 def has_sufficient_edges(self): 1868 '''Return True if we have met the maximum "from edges" criteria''' 1869 if len(self.edge_from) >= self.max_edges: 1870 return True 1871 return False 1872 1873 1874class Transport(object): 1875 """Class defines a Inter-site transport found under Sites 1876 """ 1877 1878 def __init__(self, dnstr): 1879 self.dnstr = dnstr 1880 self.options = 0 1881 self.guid = None 1882 self.name = None 1883 self.address_attr = None 1884 self.bridgehead_list = [] 1885 1886 def __str__(self): 1887 '''Debug dump string output of Transport object''' 1888 1889 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr) +\ 1890 "\n\tguid=%s" % str(self.guid) +\ 1891 "\n\toptions=%d" % self.options +\ 1892 "\n\taddress_attr=%s" % self.address_attr +\ 1893 "\n\tname=%s" % self.name +\ 1894 "".join("\n\tbridgehead_list=%s" % dnstr for dnstr in self.bridgehead_list) 1895 1896 return text 1897 1898 def load_transport(self, samdb): 1899 """Given a Transport object with an prior initialization 1900 for the object's DN, search for the DN and load attributes 1901 from the samdb. 1902 """ 1903 attrs = ["objectGUID", 1904 "options", 1905 "name", 1906 "bridgeheadServerListBL", 1907 "transportAddressAttribute"] 1908 try: 1909 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE, 1910 attrs=attrs) 1911 1912 except ldb.LdbError as e18: 1913 (enum, estr) = e18.args 1914 raise KCCError("Unable to find Transport for (%s) - (%s)" % 1915 (self.dnstr, estr)) 1916 1917 msg = res[0] 1918 self.guid = misc.GUID(samdb.schema_format_value("objectGUID", 1919 msg["objectGUID"][0])) 1920 1921 if "options" in msg: 1922 self.options = int(msg["options"][0]) 1923 1924 if "transportAddressAttribute" in msg: 1925 self.address_attr = str(msg["transportAddressAttribute"][0]) 1926 1927 if "name" in msg: 1928 self.name = str(msg["name"][0]) 1929 1930 if "bridgeheadServerListBL" in msg: 1931 for value in msg["bridgeheadServerListBL"]: 1932 dsdn = dsdb_Dn(samdb, value.decode('utf8')) 1933 dnstr = str(dsdn.dn) 1934 if dnstr not in self.bridgehead_list: 1935 self.bridgehead_list.append(dnstr) 1936 1937 1938class RepsFromTo(object): 1939 """Class encapsulation of the NDR repsFromToBlob. 1940 1941 Removes the necessity of external code having to 1942 understand about other_info or manipulation of 1943 update flags. 1944 """ 1945 def __init__(self, nc_dnstr=None, ndr_blob=None): 1946 1947 self.__dict__['to_be_deleted'] = False 1948 self.__dict__['nc_dnstr'] = nc_dnstr 1949 self.__dict__['update_flags'] = 0x0 1950 # XXX the following sounds dubious and/or better solved 1951 # elsewhere, but lets leave it for now. In particular, there 1952 # seems to be no reason for all the non-ndr generated 1953 # attributes to be handled in the round about way (e.g. 1954 # self.__dict__['to_be_deleted'] = False above). On the other 1955 # hand, it all seems to work. Hooray! Hands off!. 1956 # 1957 # WARNING: 1958 # 1959 # There is a very subtle bug here with python 1960 # and our NDR code. If you assign directly to 1961 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info) 1962 # then a proper python GC reference count is not 1963 # maintained. 1964 # 1965 # To work around this we maintain an internal 1966 # reference to "dns_name(x)" and "other_info" elements 1967 # of repsFromToBlob. This internal reference 1968 # is hidden within this class but it is why you 1969 # see statements like this below: 1970 # 1971 # self.__dict__['ndr_blob'].ctr.other_info = \ 1972 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo() 1973 # 1974 # That would appear to be a redundant assignment but 1975 # it is necessary to hold a proper python GC reference 1976 # count. 1977 if ndr_blob is None: 1978 self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob() 1979 self.__dict__['ndr_blob'].version = 0x1 1980 self.__dict__['dns_name1'] = None 1981 self.__dict__['dns_name2'] = None 1982 1983 self.__dict__['ndr_blob'].ctr.other_info = \ 1984 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo() 1985 1986 else: 1987 self.__dict__['ndr_blob'] = ndr_blob 1988 self.__dict__['other_info'] = ndr_blob.ctr.other_info 1989 1990 if ndr_blob.version == 0x1: 1991 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name 1992 self.__dict__['dns_name2'] = None 1993 else: 1994 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1 1995 self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2 1996 1997 def __str__(self): 1998 '''Debug dump string output of class''' 1999 2000 text = "%s:" % self.__class__.__name__ +\ 2001 "\n\tdnstr=%s" % self.nc_dnstr +\ 2002 "\n\tupdate_flags=0x%X" % self.update_flags +\ 2003 "\n\tversion=%d" % self.version +\ 2004 "\n\tsource_dsa_obj_guid=%s" % self.source_dsa_obj_guid +\ 2005 ("\n\tsource_dsa_invocation_id=%s" % 2006 self.source_dsa_invocation_id) +\ 2007 "\n\ttransport_guid=%s" % self.transport_guid +\ 2008 "\n\treplica_flags=0x%X" % self.replica_flags +\ 2009 ("\n\tconsecutive_sync_failures=%d" % 2010 self.consecutive_sync_failures) +\ 2011 "\n\tlast_success=%s" % self.last_success +\ 2012 "\n\tlast_attempt=%s" % self.last_attempt +\ 2013 "\n\tdns_name1=%s" % self.dns_name1 +\ 2014 "\n\tdns_name2=%s" % self.dns_name2 +\ 2015 "\n\tschedule[ " +\ 2016 "".join("0x%X " % slot for slot in self.schedule) +\ 2017 "]" 2018 2019 return text 2020 2021 def __setattr__(self, item, value): 2022 """Set an attribute and chyange update flag. 2023 2024 Be aware that setting any RepsFromTo attribute will set the 2025 drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS update flag. 2026 """ 2027 if item in ['schedule', 'replica_flags', 'transport_guid', 2028 'source_dsa_obj_guid', 'source_dsa_invocation_id', 2029 'consecutive_sync_failures', 'last_success', 2030 'last_attempt']: 2031 2032 if item in ['replica_flags']: 2033 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS 2034 elif item in ['schedule']: 2035 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE 2036 2037 setattr(self.__dict__['ndr_blob'].ctr, item, value) 2038 2039 elif item in ['dns_name1']: 2040 self.__dict__['dns_name1'] = value 2041 2042 if self.__dict__['ndr_blob'].version == 0x1: 2043 self.__dict__['ndr_blob'].ctr.other_info.dns_name = \ 2044 self.__dict__['dns_name1'] 2045 else: 2046 self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \ 2047 self.__dict__['dns_name1'] 2048 2049 elif item in ['dns_name2']: 2050 self.__dict__['dns_name2'] = value 2051 2052 if self.__dict__['ndr_blob'].version == 0x1: 2053 raise AttributeError(item) 2054 else: 2055 self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \ 2056 self.__dict__['dns_name2'] 2057 2058 elif item in ['nc_dnstr']: 2059 self.__dict__['nc_dnstr'] = value 2060 2061 elif item in ['to_be_deleted']: 2062 self.__dict__['to_be_deleted'] = value 2063 2064 elif item in ['version']: 2065 raise AttributeError("Attempt to set readonly attribute %s" % item) 2066 else: 2067 raise AttributeError("Unknown attribute %s" % item) 2068 2069 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS 2070 2071 def __getattr__(self, item): 2072 """Overload of RepsFromTo attribute retrieval. 2073 2074 Allows external code to ignore substructures within the blob 2075 """ 2076 if item in ['schedule', 'replica_flags', 'transport_guid', 2077 'source_dsa_obj_guid', 'source_dsa_invocation_id', 2078 'consecutive_sync_failures', 'last_success', 2079 'last_attempt']: 2080 return getattr(self.__dict__['ndr_blob'].ctr, item) 2081 2082 elif item in ['version']: 2083 return self.__dict__['ndr_blob'].version 2084 2085 elif item in ['dns_name1']: 2086 if self.__dict__['ndr_blob'].version == 0x1: 2087 return self.__dict__['ndr_blob'].ctr.other_info.dns_name 2088 else: 2089 return self.__dict__['ndr_blob'].ctr.other_info.dns_name1 2090 2091 elif item in ['dns_name2']: 2092 if self.__dict__['ndr_blob'].version == 0x1: 2093 raise AttributeError(item) 2094 else: 2095 return self.__dict__['ndr_blob'].ctr.other_info.dns_name2 2096 2097 elif item in ['to_be_deleted']: 2098 return self.__dict__['to_be_deleted'] 2099 2100 elif item in ['nc_dnstr']: 2101 return self.__dict__['nc_dnstr'] 2102 2103 elif item in ['update_flags']: 2104 return self.__dict__['update_flags'] 2105 2106 raise AttributeError("Unknown attribute %s" % item) 2107 2108 def is_modified(self): 2109 return (self.update_flags != 0x0) 2110 2111 def set_unmodified(self): 2112 self.__dict__['update_flags'] = 0x0 2113 2114 2115class SiteLink(object): 2116 """Class defines a site link found under sites 2117 """ 2118 2119 def __init__(self, dnstr): 2120 self.dnstr = dnstr 2121 self.options = 0 2122 self.system_flags = 0 2123 self.cost = 0 2124 self.schedule = None 2125 self.interval = None 2126 self.site_list = [] 2127 2128 def __str__(self): 2129 '''Debug dump string output of Transport object''' 2130 2131 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr) +\ 2132 "\n\toptions=%d" % self.options +\ 2133 "\n\tsystem_flags=%d" % self.system_flags +\ 2134 "\n\tcost=%d" % self.cost +\ 2135 "\n\tinterval=%s" % self.interval 2136 2137 if self.schedule is not None: 2138 text += "\n\tschedule.size=%s" % self.schedule.size +\ 2139 "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth +\ 2140 ("\n\tschedule.numberOfSchedules=%s" % 2141 self.schedule.numberOfSchedules) 2142 2143 for i, header in enumerate(self.schedule.headerArray): 2144 text += ("\n\tschedule.headerArray[%d].type=%d" % 2145 (i, header.type)) +\ 2146 ("\n\tschedule.headerArray[%d].offset=%d" % 2147 (i, header.offset)) +\ 2148 "\n\tschedule.dataArray[%d].slots[ " % i +\ 2149 "".join("0x%X " % slot for slot in self.schedule.dataArray[i].slots) +\ 2150 "]" 2151 2152 for guid, dn in self.site_list: 2153 text = text + "\n\tsite_list=%s (%s)" % (guid, dn) 2154 return text 2155 2156 def load_sitelink(self, samdb): 2157 """Given a siteLink object with an prior initialization 2158 for the object's DN, search for the DN and load attributes 2159 from the samdb. 2160 """ 2161 attrs = ["options", 2162 "systemFlags", 2163 "cost", 2164 "schedule", 2165 "replInterval", 2166 "siteList"] 2167 try: 2168 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE, 2169 attrs=attrs, controls=['extended_dn:0']) 2170 2171 except ldb.LdbError as e19: 2172 (enum, estr) = e19.args 2173 raise KCCError("Unable to find SiteLink for (%s) - (%s)" % 2174 (self.dnstr, estr)) 2175 2176 msg = res[0] 2177 2178 if "options" in msg: 2179 self.options = int(msg["options"][0]) 2180 2181 if "systemFlags" in msg: 2182 self.system_flags = int(msg["systemFlags"][0]) 2183 2184 if "cost" in msg: 2185 self.cost = int(msg["cost"][0]) 2186 2187 if "replInterval" in msg: 2188 self.interval = int(msg["replInterval"][0]) 2189 2190 if "siteList" in msg: 2191 for value in msg["siteList"]: 2192 dsdn = dsdb_Dn(samdb, value.decode('utf8')) 2193 guid = misc.GUID(dsdn.dn.get_extended_component('GUID')) 2194 dnstr = str(dsdn.dn) 2195 if (guid, dnstr) not in self.site_list: 2196 self.site_list.append((guid, dnstr)) 2197 2198 if "schedule" in msg: 2199 self.schedule = ndr_unpack(drsblobs.schedule, value) 2200 else: 2201 self.schedule = new_connection_schedule() 2202 2203 2204class KCCFailedObject(object): 2205 def __init__(self, uuid, failure_count, time_first_failure, 2206 last_result, dns_name): 2207 self.uuid = uuid 2208 self.failure_count = failure_count 2209 self.time_first_failure = time_first_failure 2210 self.last_result = last_result 2211 self.dns_name = dns_name 2212 2213 2214################################################## 2215# Global Functions and Variables 2216################################################## 2217 2218def get_dsa_config_rep(dsa): 2219 # Find configuration NC replica for the DSA 2220 for c_rep in dsa.current_rep_table.values(): 2221 if c_rep.is_config(): 2222 return c_rep 2223 2224 raise KCCError("Unable to find config NC replica for (%s)" % 2225 dsa.dsa_dnstr) 2226 2227 2228def new_connection_schedule(): 2229 """Create a default schedule for an NTDSConnection or Sitelink. This 2230 is packed differently from the repltimes schedule used elsewhere 2231 in KCC (where the 168 nibbles are packed into 84 bytes). 2232 """ 2233 # 168 byte instances of the 0x01 value. The low order 4 bits 2234 # of the byte equate to 15 minute intervals within a single hour. 2235 # There are 168 bytes because there are 168 hours in a full week 2236 # Effectively we are saying to perform replication at the end of 2237 # each hour of the week 2238 schedule = drsblobs.schedule() 2239 2240 schedule.size = 188 2241 schedule.bandwidth = 0 2242 schedule.numberOfSchedules = 1 2243 2244 header = drsblobs.scheduleHeader() 2245 header.type = 0 2246 header.offset = 20 2247 2248 schedule.headerArray = [header] 2249 2250 data = drsblobs.scheduleSlots() 2251 data.slots = [0x01] * 168 2252 2253 schedule.dataArray = [data] 2254 return schedule 2255 2256 2257################################################## 2258# DNS related calls 2259################################################## 2260 2261def uncovered_sites_to_cover(samdb, site_name): 2262 """ 2263 Discover which sites have no DCs and whose lowest single-hop cost 2264 distance for any link attached to that site is linked to the site supplied. 2265 2266 We compare the lowest cost of your single-hop link to this site to all of 2267 those available (if it exists). This means that a lower ranked siteLink 2268 with only the uncovered site can trump any available links (but this can 2269 only be done with specific, poorly enacted user configuration). 2270 2271 If the site is connected to more than one other site with the same 2272 siteLink, only the largest site (failing that sorted alphabetically) 2273 creates the DNS records. 2274 2275 :param samdb database 2276 :param site_name origin site (with a DC) 2277 2278 :return a list of sites this site should be covering (for DNS) 2279 """ 2280 sites_to_cover = [] 2281 2282 server_res = samdb.search(base=samdb.get_config_basedn(), 2283 scope=ldb.SCOPE_SUBTREE, 2284 expression="(&(objectClass=server)" 2285 "(serverReference=*))") 2286 2287 site_res = samdb.search(base=samdb.get_config_basedn(), 2288 scope=ldb.SCOPE_SUBTREE, 2289 expression="(objectClass=site)") 2290 2291 sites_in_use = Counter() 2292 dc_count = 0 2293 2294 # Assume server is of form DC,Servers,Site-ABCD because of schema 2295 for msg in server_res: 2296 site_dn = msg.dn.parent().parent() 2297 sites_in_use[site_dn.canonical_str()] += 1 2298 2299 if site_dn.get_rdn_value().lower() == site_name.lower(): 2300 dc_count += 1 2301 2302 if len(sites_in_use) != len(site_res): 2303 # There is a possible uncovered site 2304 sites_uncovered = [] 2305 2306 for msg in site_res: 2307 if msg.dn.canonical_str() not in sites_in_use: 2308 sites_uncovered.append(msg) 2309 2310 own_site_dn = "CN={},CN=Sites,{}".format( 2311 ldb.binary_encode(site_name), 2312 ldb.binary_encode(str(samdb.get_config_basedn())) 2313 ) 2314 2315 for site in sites_uncovered: 2316 encoded_dn = ldb.binary_encode(str(site.dn)) 2317 2318 # Get a sorted list of all siteLinks featuring the uncovered site 2319 link_res1 = samdb.search(base=samdb.get_config_basedn(), 2320 scope=ldb.SCOPE_SUBTREE, attrs=["cost"], 2321 expression="(&(objectClass=siteLink)" 2322 "(siteList={}))".format(encoded_dn), 2323 controls=["server_sort:1:0:cost"]) 2324 2325 # Get a sorted list of all siteLinks connecting this an the 2326 # uncovered site 2327 link_res2 = samdb.search(base=samdb.get_config_basedn(), 2328 scope=ldb.SCOPE_SUBTREE, 2329 attrs=["cost", "siteList"], 2330 expression="(&(objectClass=siteLink)" 2331 "(siteList={})(siteList={}))".format( 2332 own_site_dn, 2333 encoded_dn), 2334 controls=["server_sort:1:0:cost"]) 2335 2336 # Add to list if your link is equal in cost to lowest cost link 2337 if len(link_res1) > 0 and len(link_res2) > 0: 2338 cost1 = int(link_res1[0]['cost'][0]) 2339 cost2 = int(link_res2[0]['cost'][0]) 2340 2341 # Own siteLink must match the lowest cost link 2342 if cost1 != cost2: 2343 continue 2344 2345 # In a siteLink with more than 2 sites attached, only pick the 2346 # largest site, and if there are multiple, the earliest 2347 # alphabetically. 2348 to_cover = True 2349 for site_val in link_res2[0]['siteList']: 2350 site_dn = ldb.Dn(samdb, str(site_val)) 2351 site_dn_str = site_dn.canonical_str() 2352 site_rdn = site_dn.get_rdn_value().lower() 2353 if sites_in_use[site_dn_str] > dc_count: 2354 to_cover = False 2355 break 2356 elif (sites_in_use[site_dn_str] == dc_count and 2357 site_rdn < site_name.lower()): 2358 to_cover = False 2359 break 2360 2361 if to_cover: 2362 site_cover_rdn = site.dn.get_rdn_value() 2363 sites_to_cover.append(site_cover_rdn.lower()) 2364 2365 return sites_to_cover 2366