1 # KCC topology utilities
3 # Copyright (C) Dave Craft 2011
4 # Copyright (C) Jelmer Vernooij 2011
5 # Copyright (C) Andrew Bartlett 2015
7 # Andrew Bartlett's alleged work performed by his underlings Douglas
8 # Bagnall and Garming Sam.
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.
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.
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/>.
26 from samba import dsdb
27 from samba.dcerpc import (
32 from samba.common import dsdb_Dn
33 from samba.ndr import ndr_unpack, ndr_pack
34 from collections import Counter
37 class KCCError(Exception):
42 (unknown, schema, domain, config, application) = range(0, 5)
44 # map the NCType enum to strings for debugging
45 nctype_lut = dict((v, k) for k, v in NCType.__dict__.items() if k[:2] != '__')
48 class NamingContext(object):
49 """Base class for a naming context.
51 Holds the DN, GUID, SID (if available) and type of the DN.
52 Subclasses may inherit from this and specialize
55 def __init__(self, nc_dnstr):
56 """Instantiate a NamingContext
58 :param nc_dnstr: NC dn string
60 self.nc_dnstr = nc_dnstr
63 self.nc_type = NCType.unknown
66 '''Debug dump string output of class'''
67 text = "%s:" % (self.__class__.__name__,)
68 text = text + "\n\tnc_dnstr=%s" % self.nc_dnstr
69 text = text + "\n\tnc_guid=%s" % str(self.nc_guid)
71 if self.nc_sid is None:
72 text = text + "\n\tnc_sid=<absent>"
74 text = text + "\n\tnc_sid=<present>"
76 text = text + "\n\tnc_type=%s (%s)" % (nctype_lut[self.nc_type],
80 def load_nc(self, samdb):
81 attrs = ["objectGUID",
84 res = samdb.search(base=self.nc_dnstr,
85 scope=ldb.SCOPE_BASE, attrs=attrs)
87 except ldb.LdbError as e:
89 raise KCCError("Unable to find naming context (%s) - (%s)" %
90 (self.nc_dnstr, estr))
92 if "objectGUID" in msg:
93 self.nc_guid = misc.GUID(samdb.schema_format_value("objectGUID",
94 msg["objectGUID"][0]))
95 if "objectSid" in msg:
96 self.nc_sid = msg["objectSid"][0]
98 assert self.nc_guid is not None
101 '''Return True if NC is config'''
102 assert self.nc_type != NCType.unknown
103 return self.nc_type == NCType.config
105 def identify_by_basedn(self, samdb):
106 """Given an NC object, identify what type is is thru
107 the samdb basedn strings and NC sid value
109 # Invoke loader to initialize guid and more
110 # importantly sid value (sid is used to identify
112 if self.nc_guid is None:
115 # We check against schema and config because they
116 # will be the same for all nTDSDSAs in the forest.
117 # That leaves the domain NCs which can be identified
118 # by sid and application NCs as the last identified
119 if self.nc_dnstr == str(samdb.get_schema_basedn()):
120 self.nc_type = NCType.schema
121 elif self.nc_dnstr == str(samdb.get_config_basedn()):
122 self.nc_type = NCType.config
123 elif self.nc_sid is not None:
124 self.nc_type = NCType.domain
126 self.nc_type = NCType.application
128 def identify_by_dsa_attr(self, samdb, attr):
129 """Given an NC which has been discovered thru the
130 nTDSDSA database object, determine what type of NC
131 it is (i.e. schema, config, domain, application) via
132 the use of the schema attribute under which the NC
135 :param attr: attr of nTDSDSA object where NC DN appears
137 # If the NC is listed under msDS-HasDomainNCs then
138 # this can only be a domain NC and it is our default
139 # domain for this dsa
140 if attr == "msDS-HasDomainNCs":
141 self.nc_type = NCType.domain
143 # If the NC is listed under hasPartialReplicaNCs
144 # this is only a domain NC
145 elif attr == "hasPartialReplicaNCs":
146 self.nc_type = NCType.domain
148 # NCs listed under hasMasterNCs are either
149 # default domain, schema, or config. We
150 # utilize the identify_by_basedn() to
152 elif attr == "hasMasterNCs":
153 self.identify_by_basedn(samdb)
155 # Still unknown (unlikely) but for completeness
156 # and for finally identifying application NCs
157 if self.nc_type == NCType.unknown:
158 self.identify_by_basedn(samdb)
161 class NCReplica(NamingContext):
162 """Naming context replica that is relative to a specific DSA.
164 This is a more specific form of NamingContext class (inheriting from that
165 class) and it identifies unique attributes of the DSA's replica for a NC.
168 def __init__(self, dsa, nc_dnstr):
169 """Instantiate a Naming Context Replica
171 :param dsa_guid: GUID of DSA where replica appears
172 :param nc_dnstr: NC dn string
174 self.rep_dsa_dnstr = dsa.dsa_dnstr
175 self.rep_dsa_guid = dsa.dsa_guid
176 self.rep_default = False # replica for DSA's default domain
177 self.rep_partial = False
179 self.rep_instantiated_flags = 0
181 self.rep_fsmo_role_owner = None
184 self.rep_repsFrom = []
189 # The (is present) test is a combination of being
190 # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or
191 # hasPartialReplicaNCs) as well as its replica flags found
192 # thru the msDS-HasInstantiatedNCs. If the NC replica meets
193 # the first enumeration test then this flag is set true
194 self.rep_present_criteria_one = False
196 # Call my super class we inherited from
197 NamingContext.__init__(self, nc_dnstr)
200 '''Debug dump string output of class'''
201 text = "%s:" % self.__class__.__name__
202 text = text + "\n\tdsa_dnstr=%s" % self.rep_dsa_dnstr
203 text = text + "\n\tdsa_guid=%s" % self.rep_dsa_guid
204 text = text + "\n\tdefault=%s" % self.rep_default
205 text = text + "\n\tro=%s" % self.rep_ro
206 text = text + "\n\tpartial=%s" % self.rep_partial
207 text = text + "\n\tpresent=%s" % self.is_present()
208 text = text + "\n\tfsmo_role_owner=%s" % self.rep_fsmo_role_owner
210 for rep in self.rep_repsFrom:
211 text = text + "\n%s" % rep
213 for rep in self.rep_repsTo:
214 text = text + "\n%s" % rep
216 return "%s\n%s" % (NamingContext.__str__(self), text)
218 def set_instantiated_flags(self, flags=0):
219 '''Set or clear NC replica instantiated flags'''
220 self.rep_instantiated_flags = flags
222 def identify_by_dsa_attr(self, samdb, attr):
223 """Given an NC which has been discovered thru the
224 nTDSDSA database object, determine what type of NC
225 replica it is (i.e. partial, read only, default)
227 :param attr: attr of nTDSDSA object where NC DN appears
229 # If the NC was found under hasPartialReplicaNCs
230 # then a partial replica at this dsa
231 if attr == "hasPartialReplicaNCs":
232 self.rep_partial = True
233 self.rep_present_criteria_one = True
235 # If the NC is listed under msDS-HasDomainNCs then
236 # this can only be a domain NC and it is the DSA's
238 elif attr == "msDS-HasDomainNCs":
239 self.rep_default = True
241 # NCs listed under hasMasterNCs are either
242 # default domain, schema, or config. We check
243 # against schema and config because they will be
244 # the same for all nTDSDSAs in the forest. That
245 # leaves the default domain NC remaining which
246 # may be different for each nTDSDSAs (and thus
247 # we don't compare agains this samdb's default
249 elif attr == "hasMasterNCs":
250 self.rep_present_criteria_one = True
252 if self.nc_dnstr != str(samdb.get_schema_basedn()) and \
253 self.nc_dnstr != str(samdb.get_config_basedn()):
254 self.rep_default = True
257 elif attr == "msDS-hasFullReplicaNCs":
258 self.rep_present_criteria_one = True
262 elif attr == "msDS-hasMasterNCs":
263 self.rep_present_criteria_one = True
266 # Now use this DSA attribute to identify the naming
267 # context type by calling the super class method
269 NamingContext.identify_by_dsa_attr(self, samdb, attr)
271 def is_default(self):
272 """Whether this is a default domain for the dsa that this NC appears on
274 return self.rep_default
277 '''Return True if NC replica is read only'''
280 def is_partial(self):
281 '''Return True if NC replica is partial'''
282 return self.rep_partial
284 def is_present(self):
285 """Given an NC replica which has been discovered thru the
286 nTDSDSA database object and populated with replica flags
287 from the msDS-HasInstantiatedNCs; return whether the NC
288 replica is present (true) or if the IT_NC_GOING flag is
289 set then the NC replica is not present (false)
291 if self.rep_present_criteria_one and \
292 self.rep_instantiated_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0:
296 def load_repsFrom(self, samdb):
297 """Given an NC replica which has been discovered thru the nTDSDSA
298 database object, load the repsFrom attribute for the local replica.
299 held by my dsa. The repsFrom attribute is not replicated so this
300 attribute is relative only to the local DSA that the samdb exists on
303 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
306 except ldb.LdbError as e1:
307 (enum, estr) = e1.args
308 raise KCCError("Unable to find NC for (%s) - (%s)" %
309 (self.nc_dnstr, estr))
313 # Possibly no repsFrom if this is a singleton DC
314 if "repsFrom" in msg:
315 for value in msg["repsFrom"]:
316 rep = RepsFromTo(self.nc_dnstr,
317 ndr_unpack(drsblobs.repsFromToBlob, value))
318 self.rep_repsFrom.append(rep)
320 def commit_repsFrom(self, samdb, ro=False):
321 """Commit repsFrom to the database"""
323 # XXX - This is not truly correct according to the MS-TECH
324 # docs. To commit a repsFrom we should be using RPCs
325 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
326 # IDL_DRSReplicaDel to affect a repsFrom change.
328 # Those RPCs are missing in samba, so I'll have to
329 # implement them to get this to more accurately
330 # reflect the reference docs. As of right now this
331 # commit to the database will work as its what the
337 for repsFrom in self.rep_repsFrom:
339 # Leave out any to be deleted from
340 # replacement list. Build a list
341 # of to be deleted reps which we will
342 # remove from rep_repsFrom list below
343 if repsFrom.to_be_deleted:
344 delreps.append(repsFrom)
348 if repsFrom.is_modified():
349 repsFrom.set_unmodified()
352 # current (unmodified) elements also get
353 # appended here but no changes will occur
354 # unless something is "to be modified" or
356 newreps.append(ndr_pack(repsFrom.ndr_blob))
358 # Now delete these from our list of rep_repsFrom
359 for repsFrom in delreps:
360 self.rep_repsFrom.remove(repsFrom)
363 # Nothing to do if no reps have been modified or
364 # need to be deleted or input option has informed
365 # us to be "readonly" (ro). Leave database
371 m.dn = ldb.Dn(samdb, self.nc_dnstr)
374 ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsFrom")
379 except ldb.LdbError as estr:
380 raise KCCError("Could not set repsFrom for (%s) - (%s)" %
381 (self.nc_dnstr, estr))
383 def load_replUpToDateVector(self, samdb):
384 """Given an NC replica which has been discovered thru the nTDSDSA
385 database object, load the replUpToDateVector attribute for the
386 local replica. held by my dsa. The replUpToDateVector
387 attribute is not replicated so this attribute is relative only
388 to the local DSA that the samdb exists on
392 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
393 attrs=["replUpToDateVector"])
395 except ldb.LdbError as e2:
396 (enum, estr) = e2.args
397 raise KCCError("Unable to find NC for (%s) - (%s)" %
398 (self.nc_dnstr, estr))
402 # Possibly no replUpToDateVector if this is a singleton DC
403 if "replUpToDateVector" in msg:
404 value = msg["replUpToDateVector"][0]
405 blob = ndr_unpack(drsblobs.replUpToDateVectorBlob,
407 if blob.version != 2:
408 # Samba only generates version 2, and this runs locally
409 raise AttributeError("Unexpected replUpToDateVector version %d"
412 self.rep_replUpToDateVector_cursors = blob.ctr.cursors
414 self.rep_replUpToDateVector_cursors = []
416 def dumpstr_to_be_deleted(self):
417 return '\n'.join(str(x) for x in self.rep_repsFrom if x.to_be_deleted)
419 def dumpstr_to_be_modified(self):
420 return '\n'.join(str(x) for x in self.rep_repsFrom if x.is_modified())
422 def load_fsmo_roles(self, samdb):
423 """Given an NC replica which has been discovered thru the nTDSDSA
424 database object, load the fSMORoleOwner attribute.
427 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
428 attrs=["fSMORoleOwner"])
430 except ldb.LdbError as e3:
431 (enum, estr) = e3.args
432 raise KCCError("Unable to find NC for (%s) - (%s)" %
433 (self.nc_dnstr, estr))
437 # Possibly no fSMORoleOwner
438 if "fSMORoleOwner" in msg:
439 self.rep_fsmo_role_owner = msg["fSMORoleOwner"]
441 def is_fsmo_role_owner(self, dsa_dnstr):
442 if self.rep_fsmo_role_owner is not None and \
443 self.rep_fsmo_role_owner == dsa_dnstr:
447 def load_repsTo(self, samdb):
448 """Given an NC replica which has been discovered thru the nTDSDSA
449 database object, load the repsTo attribute for the local replica.
450 held by my dsa. The repsTo attribute is not replicated so this
451 attribute is relative only to the local DSA that the samdb exists on
453 This is responsible for push replication, not scheduled pull
454 replication. Not to be confused for repsFrom.
457 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
460 except ldb.LdbError as e4:
461 (enum, estr) = e4.args
462 raise KCCError("Unable to find NC for (%s) - (%s)" %
463 (self.nc_dnstr, estr))
467 # Possibly no repsTo if this is a singleton DC
469 for value in msg["repsTo"]:
470 rep = RepsFromTo(self.nc_dnstr,
471 ndr_unpack(drsblobs.repsFromToBlob, value))
472 self.rep_repsTo.append(rep)
474 def commit_repsTo(self, samdb, ro=False):
475 """Commit repsTo to the database"""
477 # XXX - This is not truly correct according to the MS-TECH
478 # docs. To commit a repsTo we should be using RPCs
479 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
480 # IDL_DRSReplicaDel to affect a repsTo change.
482 # Those RPCs are missing in samba, so I'll have to
483 # implement them to get this to more accurately
484 # reflect the reference docs. As of right now this
485 # commit to the database will work as its what the
491 for repsTo in self.rep_repsTo:
493 # Leave out any to be deleted from
494 # replacement list. Build a list
495 # of to be deleted reps which we will
496 # remove from rep_repsTo list below
497 if repsTo.to_be_deleted:
498 delreps.append(repsTo)
502 if repsTo.is_modified():
503 repsTo.set_unmodified()
506 # current (unmodified) elements also get
507 # appended here but no changes will occur
508 # unless something is "to be modified" or
510 newreps.append(ndr_pack(repsTo.ndr_blob))
512 # Now delete these from our list of rep_repsTo
513 for repsTo in delreps:
514 self.rep_repsTo.remove(repsTo)
517 # Nothing to do if no reps have been modified or
518 # need to be deleted or input option has informed
519 # us to be "readonly" (ro). Leave database
525 m.dn = ldb.Dn(samdb, self.nc_dnstr)
528 ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsTo")
533 except ldb.LdbError as estr:
534 raise KCCError("Could not set repsTo for (%s) - (%s)" %
535 (self.nc_dnstr, estr))
538 class DirectoryServiceAgent(object):
540 def __init__(self, dsa_dnstr):
541 """Initialize DSA class.
543 Class is subsequently fully populated by calling the load_dsa() method
545 :param dsa_dnstr: DN of the nTDSDSA
547 self.dsa_dnstr = dsa_dnstr
550 self.dsa_is_ro = False
551 self.dsa_is_istg = False
553 self.dsa_behavior = 0
554 self.default_dnstr = None # default domain dn string for dsa
556 # NCReplicas for this dsa that are "present"
557 # Indexed by DN string of naming context
558 self.current_rep_table = {}
560 # NCReplicas for this dsa that "should be present"
561 # Indexed by DN string of naming context
562 self.needed_rep_table = {}
564 # NTDSConnections for this dsa. These are current
565 # valid connections that are committed or pending a commit
566 # in the database. Indexed by DN string of connection
567 self.connect_table = {}
570 '''Debug dump string output of class'''
572 text = "%s:" % self.__class__.__name__
573 if self.dsa_dnstr is not None:
574 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
575 if self.dsa_guid is not None:
576 text = text + "\n\tdsa_guid=%s" % str(self.dsa_guid)
577 if self.dsa_ivid is not None:
578 text = text + "\n\tdsa_ivid=%s" % str(self.dsa_ivid)
580 text = text + "\n\tro=%s" % self.is_ro()
581 text = text + "\n\tgc=%s" % self.is_gc()
582 text = text + "\n\tistg=%s" % self.is_istg()
584 text = text + "\ncurrent_replica_table:"
585 text = text + "\n%s" % self.dumpstr_current_replica_table()
586 text = text + "\nneeded_replica_table:"
587 text = text + "\n%s" % self.dumpstr_needed_replica_table()
588 text = text + "\nconnect_table:"
589 text = text + "\n%s" % self.dumpstr_connect_table()
593 def get_current_replica(self, nc_dnstr):
594 return self.current_rep_table.get(nc_dnstr)
597 '''Returns True if dsa is intersite topology generator for it's site'''
598 # The KCC on an RODC always acts as an ISTG for itself
599 return self.dsa_is_istg or self.dsa_is_ro
602 '''Returns True if dsa a read only domain controller'''
603 return self.dsa_is_ro
606 '''Returns True if dsa hosts a global catalog'''
607 if (self.options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
611 def is_minimum_behavior(self, version):
612 """Is dsa at minimum windows level greater than or equal to (version)
614 :param version: Windows version to test against
615 (e.g. DS_DOMAIN_FUNCTION_2008)
617 if self.dsa_behavior >= version:
621 def is_translate_ntdsconn_disabled(self):
622 """Whether this allows NTDSConnection translation in its options."""
623 if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0:
627 def get_rep_tables(self):
628 """Return DSA current and needed replica tables
630 return self.current_rep_table, self.needed_rep_table
632 def get_parent_dnstr(self):
633 """Get the parent DN string of this object."""
634 head, sep, tail = self.dsa_dnstr.partition(',')
637 def load_dsa(self, samdb):
638 """Load a DSA from the samdb.
640 Prior initialization has given us the DN of the DSA that we are to
641 load. This method initializes all other attributes, including loading
642 the NC replica table for this DSA.
644 attrs = ["objectGUID",
648 "msDS-Behavior-Version"]
650 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
653 except ldb.LdbError as e5:
654 (enum, estr) = e5.args
655 raise KCCError("Unable to find nTDSDSA for (%s) - (%s)" %
656 (self.dsa_dnstr, estr))
659 self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID",
660 msg["objectGUID"][0]))
662 # RODCs don't originate changes and thus have no invocationId,
663 # therefore we must check for existence first
664 if "invocationId" in msg:
665 self.dsa_ivid = misc.GUID(samdb.schema_format_value("objectGUID",
666 msg["invocationId"][0]))
669 self.options = int(msg["options"][0])
671 if "msDS-isRODC" in msg and msg["msDS-isRODC"][0] == "TRUE":
672 self.dsa_is_ro = True
674 self.dsa_is_ro = False
676 if "msDS-Behavior-Version" in msg:
677 self.dsa_behavior = int(msg['msDS-Behavior-Version'][0])
679 # Load the NC replicas that are enumerated on this dsa
680 self.load_current_replica_table(samdb)
682 # Load the nTDSConnection that are enumerated on this dsa
683 self.load_connection_table(samdb)
685 def load_current_replica_table(self, samdb):
686 """Method to load the NC replica's listed for DSA object.
688 This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs,
689 hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, and
690 msDS-HasInstantiatedNCs) to determine complete list of NC replicas that
691 are enumerated for the DSA. Once a NC replica is loaded it is
692 identified (schema, config, etc) and the other replica attributes
693 (partial, ro, etc) are determined.
695 :param samdb: database to query for DSA replica list
698 # not RODC - default, config, schema (old style)
700 # not RODC - default, config, schema, app NCs
702 # domain NC partial replicas
703 "hasPartialReplicaNCs",
706 # RODC only - default, config, schema, app NCs
707 "msDS-hasFullReplicaNCs",
708 # Identifies if replica is coming, going, or stable
709 "msDS-HasInstantiatedNCs"
712 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
715 except ldb.LdbError as e6:
716 (enum, estr) = e6.args
717 raise KCCError("Unable to find nTDSDSA NCs for (%s) - (%s)" %
718 (self.dsa_dnstr, estr))
720 # The table of NCs for the dsa we are searching
723 # We should get one response to our query here for
724 # the ntds that we requested
727 # Our response will contain a number of elements including
728 # the dn of the dsa as well as elements for each
729 # attribute (e.g. hasMasterNCs). Each of these elements
730 # is a dictonary list which we retrieve the keys for and
731 # then iterate over them
732 for k in res[0].keys():
736 # For each attribute type there will be one or more DNs
737 # listed. For instance DCs normally have 3 hasMasterNCs
739 for value in res[0][k]:
740 # Turn dn into a dsdb_Dn so we can use
741 # its methods to parse a binary DN
742 dsdn = dsdb_Dn(samdb, value)
743 flags = dsdn.get_binary_integer()
746 if not dnstr in tmp_table:
747 rep = NCReplica(self, dnstr)
748 tmp_table[dnstr] = rep
750 rep = tmp_table[dnstr]
752 if k == "msDS-HasInstantiatedNCs":
753 rep.set_instantiated_flags(flags)
756 rep.identify_by_dsa_attr(samdb, k)
758 # if we've identified the default domain NC
759 # then save its DN string
761 self.default_dnstr = dnstr
763 raise KCCError("No nTDSDSA NCs for (%s)" % self.dsa_dnstr)
765 # Assign our newly built NC replica table to this dsa
766 self.current_rep_table = tmp_table
768 def add_needed_replica(self, rep):
769 """Method to add a NC replica that "should be present" to the
772 self.needed_rep_table[rep.nc_dnstr] = rep
774 def load_connection_table(self, samdb):
775 """Method to load the nTDSConnections listed for DSA object.
777 :param samdb: database to query for DSA connection list
780 res = samdb.search(base=self.dsa_dnstr,
781 scope=ldb.SCOPE_SUBTREE,
782 expression="(objectClass=nTDSConnection)")
784 except ldb.LdbError as e7:
785 (enum, estr) = e7.args
786 raise KCCError("Unable to find nTDSConnection for (%s) - (%s)" %
787 (self.dsa_dnstr, estr))
793 if dnstr in self.connect_table:
796 connect = NTDSConnection(dnstr)
798 connect.load_connection(samdb)
799 self.connect_table[dnstr] = connect
801 def commit_connections(self, samdb, ro=False):
802 """Method to commit any uncommitted nTDSConnections
803 modifications that are in our table. These would be
804 identified connections that are marked to be added or
807 :param samdb: database to commit DSA connection list to
808 :param ro: if (true) then peform internal operations but
809 do not write to the database (readonly)
813 for dnstr, connect in self.connect_table.items():
814 if connect.to_be_added:
815 connect.commit_added(samdb, ro)
817 if connect.to_be_modified:
818 connect.commit_modified(samdb, ro)
820 if connect.to_be_deleted:
821 connect.commit_deleted(samdb, ro)
822 delconn.append(dnstr)
824 # Now delete the connection from the table
825 for dnstr in delconn:
826 del self.connect_table[dnstr]
828 def add_connection(self, dnstr, connect):
829 assert dnstr not in self.connect_table
830 self.connect_table[dnstr] = connect
832 def get_connection_by_from_dnstr(self, from_dnstr):
833 """Scan DSA nTDSConnection table and return connection
834 with a "fromServer" dn string equivalent to method
837 :param from_dnstr: search for this from server entry
840 for connect in self.connect_table.values():
841 if connect.get_from_dnstr() == from_dnstr:
842 answer.append(connect)
846 def dumpstr_current_replica_table(self):
847 '''Debug dump string output of current replica table'''
848 return '\n'.join(str(x) for x in self.current_rep_table)
850 def dumpstr_needed_replica_table(self):
851 '''Debug dump string output of needed replica table'''
852 return '\n'.join(str(x) for x in self.needed_rep_table)
854 def dumpstr_connect_table(self):
855 '''Debug dump string output of connect table'''
856 return '\n'.join(str(x) for x in self.connect_table)
858 def new_connection(self, options, system_flags, transport, from_dnstr,
860 """Set up a new connection for the DSA based on input
861 parameters. Connection will be added to the DSA
862 connect_table and will be marked as "to be added" pending
863 a call to commit_connections()
865 dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
867 connect = NTDSConnection(dnstr)
868 connect.to_be_added = True
869 connect.enabled = True
870 connect.from_dnstr = from_dnstr
871 connect.options = options
872 connect.system_flags = system_flags
874 if transport is not None:
875 connect.transport_dnstr = transport.dnstr
876 connect.transport_guid = transport.guid
878 if sched is not None:
879 connect.schedule = sched
881 # Create schedule. Attribute valuse set according to MS-TECH
882 # intrasite connection creation document
883 connect.schedule = new_connection_schedule()
885 self.add_connection(dnstr, connect)
889 class NTDSConnection(object):
890 """Class defines a nTDSConnection found under a DSA
892 def __init__(self, dnstr):
897 self.to_be_added = False # new connection needs to be added
898 self.to_be_deleted = False # old connection needs to be deleted
899 self.to_be_modified = False
901 self.system_flags = 0
902 self.transport_dnstr = None
903 self.transport_guid = None
904 self.from_dnstr = None
908 '''Debug dump string output of NTDSConnection object'''
910 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
911 text = text + "\n\tenabled=%s" % self.enabled
912 text = text + "\n\tto_be_added=%s" % self.to_be_added
913 text = text + "\n\tto_be_deleted=%s" % self.to_be_deleted
914 text = text + "\n\tto_be_modified=%s" % self.to_be_modified
915 text = text + "\n\toptions=0x%08X" % self.options
916 text = text + "\n\tsystem_flags=0x%08X" % self.system_flags
917 text = text + "\n\twhenCreated=%d" % self.whenCreated
918 text = text + "\n\ttransport_dn=%s" % self.transport_dnstr
920 if self.guid is not None:
921 text = text + "\n\tguid=%s" % str(self.guid)
923 if self.transport_guid is not None:
924 text = text + "\n\ttransport_guid=%s" % str(self.transport_guid)
926 text = text + "\n\tfrom_dn=%s" % self.from_dnstr
928 if self.schedule is not None:
929 text += "\n\tschedule.size=%s" % self.schedule.size
930 text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
931 text += ("\n\tschedule.numberOfSchedules=%s" %
932 self.schedule.numberOfSchedules)
934 for i, header in enumerate(self.schedule.headerArray):
935 text += ("\n\tschedule.headerArray[%d].type=%d" %
937 text += ("\n\tschedule.headerArray[%d].offset=%d" %
939 text += "\n\tschedule.dataArray[%d].slots[ " % i
940 for slot in self.schedule.dataArray[i].slots:
941 text = text + "0x%X " % slot
946 def load_connection(self, samdb):
947 """Given a NTDSConnection object with an prior initialization
948 for the object's DN, search for the DN and load attributes
960 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
963 except ldb.LdbError as e8:
964 (enum, estr) = e8.args
965 raise KCCError("Unable to find nTDSConnection for (%s) - (%s)" %
971 self.options = int(msg["options"][0])
973 if "enabledConnection" in msg:
974 if msg["enabledConnection"][0].upper().lstrip().rstrip() == "TRUE":
977 if "systemFlags" in msg:
978 self.system_flags = int(msg["systemFlags"][0])
982 misc.GUID(samdb.schema_format_value("objectGUID",
983 msg["objectGUID"][0]))
985 raise KCCError("Unable to find objectGUID in nTDSConnection "
986 "for (%s)" % (self.dnstr))
988 if "transportType" in msg:
989 dsdn = dsdb_Dn(samdb, msg["transportType"][0])
990 self.load_connection_transport(samdb, str(dsdn.dn))
992 if "schedule" in msg:
993 self.schedule = ndr_unpack(drsblobs.schedule, msg["schedule"][0])
995 if "whenCreated" in msg:
996 self.whenCreated = ldb.string_to_time(msg["whenCreated"][0])
998 if "fromServer" in msg:
999 dsdn = dsdb_Dn(samdb, msg["fromServer"][0])
1000 self.from_dnstr = str(dsdn.dn)
1001 assert self.from_dnstr is not None
1003 def load_connection_transport(self, samdb, tdnstr):
1004 """Given a NTDSConnection object which enumerates a transport
1005 DN, load the transport information for the connection object
1007 :param tdnstr: transport DN to load
1009 attrs = ["objectGUID"]
1011 res = samdb.search(base=tdnstr,
1012 scope=ldb.SCOPE_BASE, attrs=attrs)
1014 except ldb.LdbError as e9:
1015 (enum, estr) = e9.args
1016 raise KCCError("Unable to find transport (%s) - (%s)" %
1019 if "objectGUID" in res[0]:
1021 self.transport_dnstr = tdnstr
1022 self.transport_guid = \
1023 misc.GUID(samdb.schema_format_value("objectGUID",
1024 msg["objectGUID"][0]))
1025 assert self.transport_dnstr is not None
1026 assert self.transport_guid is not None
1028 def commit_deleted(self, samdb, ro=False):
1029 """Local helper routine for commit_connections() which
1030 handles committed connections that are to be deleted from
1031 the database database
1033 assert self.to_be_deleted
1034 self.to_be_deleted = False
1036 # No database modification requested
1041 samdb.delete(self.dnstr)
1042 except ldb.LdbError as e10:
1043 (enum, estr) = e10.args
1044 raise KCCError("Could not delete nTDSConnection for (%s) - (%s)" %
1047 def commit_added(self, samdb, ro=False):
1048 """Local helper routine for commit_connections() which
1049 handles committed connections that are to be added to the
1052 assert self.to_be_added
1053 self.to_be_added = False
1055 # No database modification requested
1059 # First verify we don't have this entry to ensure nothing
1060 # is programatically amiss
1063 msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1067 except ldb.LdbError as e11:
1068 (enum, estr) = e11.args
1069 if enum != ldb.ERR_NO_SUCH_OBJECT:
1070 raise KCCError("Unable to search for (%s) - (%s)" %
1073 raise KCCError("nTDSConnection for (%s) already exists!" %
1081 # Prepare a message for adding to the samdb
1083 m.dn = ldb.Dn(samdb, self.dnstr)
1085 m["objectClass"] = \
1086 ldb.MessageElement("nTDSConnection", ldb.FLAG_MOD_ADD,
1088 m["showInAdvancedViewOnly"] = \
1089 ldb.MessageElement("TRUE", ldb.FLAG_MOD_ADD,
1090 "showInAdvancedViewOnly")
1091 m["enabledConnection"] = \
1092 ldb.MessageElement(enablestr, ldb.FLAG_MOD_ADD,
1093 "enabledConnection")
1095 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_ADD, "fromServer")
1097 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options")
1098 m["systemFlags"] = \
1099 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_ADD,
1102 if self.transport_dnstr is not None:
1103 m["transportType"] = \
1104 ldb.MessageElement(str(self.transport_dnstr), ldb.FLAG_MOD_ADD,
1107 if self.schedule is not None:
1109 ldb.MessageElement(ndr_pack(self.schedule),
1110 ldb.FLAG_MOD_ADD, "schedule")
1113 except ldb.LdbError as e12:
1114 (enum, estr) = e12.args
1115 raise KCCError("Could not add nTDSConnection for (%s) - (%s)" %
1118 def commit_modified(self, samdb, ro=False):
1119 """Local helper routine for commit_connections() which
1120 handles committed connections that are to be modified to the
1123 assert self.to_be_modified
1124 self.to_be_modified = False
1126 # No database modification requested
1130 # First verify we have this entry to ensure nothing
1131 # is programatically amiss
1133 # we don't use the search result, but it tests the status
1134 # of self.dnstr in the database.
1135 samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1137 except ldb.LdbError as e13:
1138 (enum, estr) = e13.args
1139 if enum == ldb.ERR_NO_SUCH_OBJECT:
1140 raise KCCError("nTDSConnection for (%s) doesn't exist!" %
1142 raise KCCError("Unable to search for (%s) - (%s)" %
1150 # Prepare a message for modifying the samdb
1152 m.dn = ldb.Dn(samdb, self.dnstr)
1154 m["enabledConnection"] = \
1155 ldb.MessageElement(enablestr, ldb.FLAG_MOD_REPLACE,
1156 "enabledConnection")
1158 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_REPLACE,
1161 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_REPLACE,
1163 m["systemFlags"] = \
1164 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_REPLACE,
1167 if self.transport_dnstr is not None:
1168 m["transportType"] = \
1169 ldb.MessageElement(str(self.transport_dnstr),
1170 ldb.FLAG_MOD_REPLACE, "transportType")
1172 m["transportType"] = \
1173 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "transportType")
1175 if self.schedule is not None:
1177 ldb.MessageElement(ndr_pack(self.schedule),
1178 ldb.FLAG_MOD_REPLACE, "schedule")
1181 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "schedule")
1184 except ldb.LdbError as e14:
1185 (enum, estr) = e14.args
1186 raise KCCError("Could not modify nTDSConnection for (%s) - (%s)" %
1189 def set_modified(self, truefalse):
1190 self.to_be_modified = truefalse
1192 def is_schedule_minimum_once_per_week(self):
1193 """Returns True if our schedule includes at least one
1194 replication interval within the week. False otherwise
1196 # replinfo schedule is None means "always", while
1197 # NTDSConnection schedule is None means "never".
1198 if self.schedule is None or self.schedule.dataArray[0] is None:
1201 for slot in self.schedule.dataArray[0].slots:
1202 if (slot & 0x0F) != 0x0:
1206 def is_equivalent_schedule(self, sched):
1207 """Returns True if our schedule is equivalent to the input
1208 comparison schedule.
1210 :param shed: schedule to compare to
1212 # There are 4 cases, where either self.schedule or sched can be None
1214 # | self. is None | self. is not None
1215 # --------------+-----------------+--------------------
1216 # sched is None | True | False
1217 # --------------+-----------------+--------------------
1218 # sched is not None | False | do calculations
1220 if self.schedule is None:
1221 return sched is None
1226 if ((self.schedule.size != sched.size or
1227 self.schedule.bandwidth != sched.bandwidth or
1228 self.schedule.numberOfSchedules != sched.numberOfSchedules)):
1231 for i, header in enumerate(self.schedule.headerArray):
1233 if self.schedule.headerArray[i].type != sched.headerArray[i].type:
1236 if self.schedule.headerArray[i].offset != \
1237 sched.headerArray[i].offset:
1240 for a, b in zip(self.schedule.dataArray[i].slots,
1241 sched.dataArray[i].slots):
1246 def is_rodc_topology(self):
1247 """Returns True if NTDS Connection specifies RODC
1250 if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
1254 def is_generated(self):
1255 """Returns True if NTDS Connection was generated by the
1256 KCC topology algorithm as opposed to set by the administrator
1258 if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0:
1262 def is_override_notify_default(self):
1263 """Returns True if NTDS Connection should override notify default
1265 if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0:
1269 def is_use_notify(self):
1270 """Returns True if NTDS Connection should use notify
1272 if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0:
1276 def is_twoway_sync(self):
1277 """Returns True if NTDS Connection should use twoway sync
1279 if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0:
1283 def is_intersite_compression_disabled(self):
1284 """Returns True if NTDS Connection intersite compression
1287 if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0:
1291 def is_user_owned_schedule(self):
1292 """Returns True if NTDS Connection has a user owned schedule
1294 if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0:
1298 def is_enabled(self):
1299 """Returns True if NTDS Connection is enabled
1303 def get_from_dnstr(self):
1304 '''Return fromServer dn string attribute'''
1305 return self.from_dnstr
1308 class Partition(NamingContext):
1309 """A naming context discovered thru Partitions DN of the config schema.
1311 This is a more specific form of NamingContext class (inheriting from that
1312 class) and it identifies unique attributes enumerated in the Partitions
1313 such as which nTDSDSAs are cross referenced for replicas
1315 def __init__(self, partstr):
1316 self.partstr = partstr
1318 self.system_flags = 0
1319 self.rw_location_list = []
1320 self.ro_location_list = []
1322 # We don't have enough info to properly
1323 # fill in the naming context yet. We'll get that
1324 # fully set up with load_partition().
1325 NamingContext.__init__(self, None)
1327 def load_partition(self, samdb):
1328 """Given a Partition class object that has been initialized with its
1329 partition dn string, load the partition from the sam database, identify
1330 the type of the partition (schema, domain, etc) and record the list of
1331 nTDSDSAs that appear in the cross reference attributes
1332 msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations.
1334 :param samdb: sam database to load partition from
1339 "msDS-NC-Replica-Locations",
1340 "msDS-NC-RO-Replica-Locations"]
1342 res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE,
1345 except ldb.LdbError as e15:
1346 (enum, estr) = e15.args
1347 raise KCCError("Unable to find partition for (%s) - (%s)" %
1348 (self.partstr, estr))
1350 for k in msg.keys():
1355 if msg[k][0].upper().lstrip().rstrip() == "TRUE":
1358 self.enabled = False
1361 if k == "systemFlags":
1362 self.system_flags = int(msg[k][0])
1365 for value in msg[k]:
1366 dsdn = dsdb_Dn(samdb, value)
1367 dnstr = str(dsdn.dn)
1370 self.nc_dnstr = dnstr
1373 if k == "msDS-NC-Replica-Locations":
1374 self.rw_location_list.append(dnstr)
1377 if k == "msDS-NC-RO-Replica-Locations":
1378 self.ro_location_list.append(dnstr)
1381 # Now identify what type of NC this partition
1383 self.identify_by_basedn(samdb)
1385 def is_enabled(self):
1386 """Returns True if partition is enabled
1388 return self.is_enabled
1390 def is_foreign(self):
1391 """Returns True if this is not an Active Directory NC in our
1392 forest but is instead something else (e.g. a foreign NC)
1394 if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0:
1399 def should_be_present(self, target_dsa):
1400 """Tests whether this partition should have an NC replica
1401 on the target dsa. This method returns a tuple of
1402 needed=True/False, ro=True/False, partial=True/False
1404 :param target_dsa: should NC be present on target dsa
1409 # If this is the config, schema, or default
1410 # domain NC for the target dsa then it should
1412 needed = (self.nc_type == NCType.config or
1413 self.nc_type == NCType.schema or
1414 (self.nc_type == NCType.domain and
1415 self.nc_dnstr == target_dsa.default_dnstr))
1417 # A writable replica of an application NC should be present
1418 # if there a cross reference to the target DSA exists. Depending
1419 # on whether the DSA is ro we examine which type of cross reference
1420 # to look for (msDS-NC-Replica-Locations or
1421 # msDS-NC-RO-Replica-Locations
1422 if self.nc_type == NCType.application:
1423 if target_dsa.is_ro():
1424 if target_dsa.dsa_dnstr in self.ro_location_list:
1427 if target_dsa.dsa_dnstr in self.rw_location_list:
1430 # If the target dsa is a gc then a partial replica of a
1431 # domain NC (other than the DSAs default domain) should exist
1432 # if there is also a cross reference for the DSA
1433 if (target_dsa.is_gc() and
1434 self.nc_type == NCType.domain and
1435 self.nc_dnstr != target_dsa.default_dnstr and
1436 (target_dsa.dsa_dnstr in self.ro_location_list or
1437 target_dsa.dsa_dnstr in self.rw_location_list)):
1441 # partial NCs are always readonly
1442 if needed and (target_dsa.is_ro() or partial):
1445 return needed, ro, partial
1448 '''Debug dump string output of class'''
1449 text = "%s" % NamingContext.__str__(self)
1450 text = text + "\n\tpartdn=%s" % self.partstr
1451 for k in self.rw_location_list:
1452 text = text + "\n\tmsDS-NC-Replica-Locations=%s" % k
1453 for k in self.ro_location_list:
1454 text = text + "\n\tmsDS-NC-RO-Replica-Locations=%s" % k
1459 """An individual site object discovered thru the configuration
1460 naming context. Contains all DSAs that exist within the site
1462 def __init__(self, site_dnstr, nt_now):
1463 self.site_dnstr = site_dnstr
1464 self.site_guid = None
1465 self.site_options = 0
1466 self.site_topo_generator = None
1467 self.site_topo_failover = 0 # appears to be in minutes
1469 self.rw_dsa_table = {}
1470 self.nt_now = nt_now
1472 def load_site(self, samdb):
1473 """Loads the NTDS Site Settings options attribute for the site
1474 as well as querying and loading all DSAs that appear within
1477 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1479 "interSiteTopologyFailover",
1480 "interSiteTopologyGenerator"]
1482 res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE,
1484 self_res = samdb.search(base=self.site_dnstr, scope=ldb.SCOPE_BASE,
1485 attrs=['objectGUID'])
1486 except ldb.LdbError as e16:
1487 (enum, estr) = e16.args
1488 raise KCCError("Unable to find site settings for (%s) - (%s)" %
1492 if "options" in msg:
1493 self.site_options = int(msg["options"][0])
1495 if "interSiteTopologyGenerator" in msg:
1496 self.site_topo_generator = \
1497 str(msg["interSiteTopologyGenerator"][0])
1499 if "interSiteTopologyFailover" in msg:
1500 self.site_topo_failover = int(msg["interSiteTopologyFailover"][0])
1503 if "objectGUID" in msg:
1504 self.site_guid = misc.GUID(samdb.schema_format_value("objectGUID",
1505 msg["objectGUID"][0]))
1507 self.load_all_dsa(samdb)
1509 def load_all_dsa(self, samdb):
1510 """Discover all nTDSDSA thru the sites entry and
1511 instantiate and load the DSAs. Each dsa is inserted
1512 into the dsa_table by dn string.
1515 res = samdb.search(self.site_dnstr,
1516 scope=ldb.SCOPE_SUBTREE,
1517 expression="(objectClass=nTDSDSA)")
1518 except ldb.LdbError as e17:
1519 (enum, estr) = e17.args
1520 raise KCCError("Unable to find nTDSDSAs - (%s)" % estr)
1526 if dnstr in self.dsa_table:
1529 dsa = DirectoryServiceAgent(dnstr)
1533 # Assign this dsa to my dsa table
1534 # and index by dsa dn
1535 self.dsa_table[dnstr] = dsa
1537 self.rw_dsa_table[dnstr] = dsa
1539 def get_dsa(self, dnstr):
1540 """Return a previously loaded DSA object by consulting
1541 the sites dsa_table for the provided DSA dn string
1543 :return: None if DSA doesn't exist
1545 return self.dsa_table.get(dnstr)
1547 def select_istg(self, samdb, mydsa, ro):
1548 """Determine if my DC should be an intersite topology
1549 generator. If my DC is the istg and is both a writeable
1550 DC and the database is opened in write mode then we perform
1551 an originating update to set the interSiteTopologyGenerator
1552 attribute in the NTDS Site Settings object. An RODC always
1553 acts as an ISTG for itself.
1555 # The KCC on an RODC always acts as an ISTG for itself
1557 mydsa.dsa_is_istg = True
1558 self.site_topo_generator = mydsa.dsa_dnstr
1561 c_rep = get_dsa_config_rep(mydsa)
1563 # Load repsFrom and replUpToDateVector if not already loaded
1564 # so we can get the current state of the config replica and
1565 # whether we are getting updates from the istg
1566 c_rep.load_repsFrom(samdb)
1568 c_rep.load_replUpToDateVector(samdb)
1570 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1571 # First, the KCC on a writable DC determines whether it acts
1572 # as an ISTG for its site
1574 # Let s be the object such that s!lDAPDisplayName = nTDSDSA
1575 # and classSchema in s!objectClass.
1577 # Let D be the sequence of objects o in the site of the local
1578 # DC such that o!objectCategory = s. D is sorted in ascending
1579 # order by objectGUID.
1581 # Which is a fancy way of saying "sort all the nTDSDSA objects
1582 # in the site by guid in ascending order". Place sorted list
1584 D_sort = sorted(self.rw_dsa_table.values(), cmp=sort_dsa_by_guid)
1586 # double word number of 100 nanosecond intervals since 1600s
1588 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1589 # if o!interSiteTopologyFailover is 0 or has no value.
1591 # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1592 # so it appears we have to turn f into the same interval
1594 # interSiteTopologyFailover (if set) appears to be in minutes
1595 # so we'll need to convert to senconds and then 100 nanosecond
1597 # XXX [MS-ADTS] 6.2.2.3.1 says it is seconds, not minutes.
1599 # 10,000,000 is number of 100 nanosecond intervals in a second
1600 if self.site_topo_failover == 0:
1601 f = 2 * 60 * 60 * 10000000
1603 f = self.site_topo_failover * 60 * 10000000
1605 # Let o be the site settings object for the site of the local
1606 # DC, or NULL if no such o exists.
1607 d_dsa = self.dsa_table.get(self.site_topo_generator)
1609 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1610 # If o != NULL and o!interSiteTopologyGenerator is not the
1611 # nTDSDSA object for the local DC and
1612 # o!interSiteTopologyGenerator is an element dj of sequence D:
1614 if d_dsa is not None and d_dsa is not mydsa:
1615 # From MS-ADTS 6.2.2.3.1 ISTG Selection:
1616 # Let c be the cursor in the replUpToDateVector variable
1617 # associated with the NC replica of the config NC such
1618 # that c.uuidDsa = dj!invocationId. If no such c exists
1619 # (No evidence of replication from current ITSG):
1623 # Else if the current time < c.timeLastSyncSuccess - f
1624 # (Evidence of time sync problem on current ISTG):
1628 # Else (Evidence of replication from current ITSG):
1630 # Let t = c.timeLastSyncSuccess.
1632 # last_success appears to be a double word containing
1633 # number of 100 nanosecond intervals since the 1600s
1634 j_idx = D_sort.index(d_dsa)
1637 for cursor in c_rep.rep_replUpToDateVector_cursors:
1638 if d_dsa.dsa_ivid == cursor.source_dsa_invocation_id:
1646 #XXX doc says current time < c.timeLastSyncSuccess - f
1647 # which is true only if f is negative or clocks are wrong.
1648 # f is not negative in the default case (2 hours).
1649 elif self.nt_now - cursor.last_sync_success > f:
1654 t_time = cursor.last_sync_success
1656 # Otherwise (Nominate local DC as ISTG):
1657 # Let i be the integer such that di is the nTDSDSA
1658 # object for the local DC.
1659 # Let t = the current time.
1661 i_idx = D_sort.index(mydsa)
1662 t_time = self.nt_now
1664 # Compute a function that maintains the current ISTG if
1665 # it is alive, cycles through other candidates if not.
1667 # Let k be the integer (i + ((current time - t) /
1668 # o!interSiteTopologyFailover)) MOD |D|.
1670 # Note: We don't want to divide by zero here so they must
1671 # have meant "f" instead of "o!interSiteTopologyFailover"
1672 k_idx = (i_idx + ((self.nt_now - t_time) // f)) % len(D_sort)
1674 # The local writable DC acts as an ISTG for its site if and
1675 # only if dk is the nTDSDSA object for the local DC. If the
1676 # local DC does not act as an ISTG, the KCC skips the
1677 # remainder of this task.
1678 d_dsa = D_sort[k_idx]
1679 d_dsa.dsa_is_istg = True
1681 # Update if we are the ISTG, otherwise return
1682 if d_dsa is not mydsa:
1686 if self.site_topo_generator == mydsa.dsa_dnstr:
1689 self.site_topo_generator = mydsa.dsa_dnstr
1691 # If readonly database then do not perform a
1696 # Perform update to the samdb
1697 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1700 m.dn = ldb.Dn(samdb, ssdn)
1702 m["interSiteTopologyGenerator"] = \
1703 ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
1704 "interSiteTopologyGenerator")
1708 except ldb.LdbError as estr:
1710 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1714 def is_intrasite_topology_disabled(self):
1715 '''Returns True if intra-site topology is disabled for site'''
1716 return (self.site_options &
1717 dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0
1719 def is_intersite_topology_disabled(self):
1720 '''Returns True if inter-site topology is disabled for site'''
1721 return ((self.site_options &
1722 dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED)
1725 def is_random_bridgehead_disabled(self):
1726 '''Returns True if selection of random bridgehead is disabled'''
1727 return (self.site_options &
1728 dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0
1730 def is_detect_stale_disabled(self):
1731 '''Returns True if detect stale is disabled for site'''
1732 return (self.site_options &
1733 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0
1735 def is_cleanup_ntdsconn_disabled(self):
1736 '''Returns True if NTDS Connection cleanup is disabled for site'''
1737 return (self.site_options &
1738 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0
1740 def same_site(self, dsa):
1741 '''Return True if dsa is in this site'''
1742 if self.get_dsa(dsa.dsa_dnstr):
1746 def is_rodc_site(self):
1747 if len(self.dsa_table) > 0 and len(self.rw_dsa_table) == 0:
1752 '''Debug dump string output of class'''
1753 text = "%s:" % self.__class__.__name__
1754 text = text + "\n\tdn=%s" % self.site_dnstr
1755 text = text + "\n\toptions=0x%X" % self.site_options
1756 text = text + "\n\ttopo_generator=%s" % self.site_topo_generator
1757 text = text + "\n\ttopo_failover=%d" % self.site_topo_failover
1758 for key, dsa in self.dsa_table.items():
1759 text = text + "\n%s" % dsa
1763 class GraphNode(object):
1764 """A graph node describing a set of edges that should be directed to it.
1766 Each edge is a connection for a particular naming context replica directed
1767 from another node in the forest to this node.
1770 def __init__(self, dsa_dnstr, max_node_edges):
1771 """Instantiate the graph node according to a DSA dn string
1773 :param max_node_edges: maximum number of edges that should ever
1774 be directed to the node
1776 self.max_edges = max_node_edges
1777 self.dsa_dnstr = dsa_dnstr
1781 text = "%s:" % self.__class__.__name__
1782 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
1783 text = text + "\n\tmax_edges=%d" % self.max_edges
1785 for i, edge in enumerate(self.edge_from):
1786 if isinstance(edge, str):
1787 text += "\n\tedge_from[%d]=%s" % (i, edge)
1791 def add_edge_from(self, from_dsa_dnstr):
1792 """Add an edge from the dsa to our graph nodes edge from list
1794 :param from_dsa_dnstr: the dsa that the edge emanates from
1796 assert isinstance(from_dsa_dnstr, str)
1798 # No edges from myself to myself
1799 if from_dsa_dnstr == self.dsa_dnstr:
1801 # Only one edge from a particular node
1802 if from_dsa_dnstr in self.edge_from:
1804 # Not too many edges
1805 if len(self.edge_from) >= self.max_edges:
1807 self.edge_from.append(from_dsa_dnstr)
1810 def add_edges_from_connections(self, dsa):
1811 """For each nTDSConnection object associated with a particular
1812 DSA, we test if it implies an edge to this graph node (i.e.
1813 the "fromServer" attribute). If it does then we add an
1814 edge from the server unless we are over the max edges for this
1817 :param dsa: dsa with a dnstr equivalent to his graph node
1819 for connect in dsa.connect_table.values():
1820 self.add_edge_from(connect.from_dnstr)
1822 def add_connections_from_edges(self, dsa, transport):
1823 """For each edge directed to this graph node, ensure there
1824 is a corresponding nTDSConnection object in the dsa.
1826 for edge_dnstr in self.edge_from:
1827 connections = dsa.get_connection_by_from_dnstr(edge_dnstr)
1829 # For each edge directed to the NC replica that
1830 # "should be present" on the local DC, the KCC determines
1831 # whether an object c exists such that:
1833 # c is a child of the DC's nTDSDSA object.
1834 # c.objectCategory = nTDSConnection
1836 # Given the NC replica ri from which the edge is directed,
1837 # c.fromServer is the dsname of the nTDSDSA object of
1838 # the DC on which ri "is present".
1840 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1843 for connect in connections:
1844 if connect.is_rodc_topology():
1851 # if no such object exists then the KCC adds an object
1852 # c with the following attributes
1854 # Generate a new dnstr for this nTDSConnection
1855 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1856 flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1857 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1859 dsa.new_connection(opt, flags, transport, edge_dnstr, None)
1861 def has_sufficient_edges(self):
1862 '''Return True if we have met the maximum "from edges" criteria'''
1863 if len(self.edge_from) >= self.max_edges:
1868 class Transport(object):
1869 """Class defines a Inter-site transport found under Sites
1872 def __init__(self, dnstr):
1877 self.address_attr = None
1878 self.bridgehead_list = []
1881 '''Debug dump string output of Transport object'''
1883 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
1884 text = text + "\n\tguid=%s" % str(self.guid)
1885 text = text + "\n\toptions=%d" % self.options
1886 text = text + "\n\taddress_attr=%s" % self.address_attr
1887 text = text + "\n\tname=%s" % self.name
1888 for dnstr in self.bridgehead_list:
1889 text = text + "\n\tbridgehead_list=%s" % dnstr
1893 def load_transport(self, samdb):
1894 """Given a Transport object with an prior initialization
1895 for the object's DN, search for the DN and load attributes
1898 attrs = ["objectGUID",
1901 "bridgeheadServerListBL",
1902 "transportAddressAttribute"]
1904 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
1907 except ldb.LdbError as e18:
1908 (enum, estr) = e18.args
1909 raise KCCError("Unable to find Transport for (%s) - (%s)" %
1913 self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
1914 msg["objectGUID"][0]))
1916 if "options" in msg:
1917 self.options = int(msg["options"][0])
1919 if "transportAddressAttribute" in msg:
1920 self.address_attr = str(msg["transportAddressAttribute"][0])
1923 self.name = str(msg["name"][0])
1925 if "bridgeheadServerListBL" in msg:
1926 for value in msg["bridgeheadServerListBL"]:
1927 dsdn = dsdb_Dn(samdb, value)
1928 dnstr = str(dsdn.dn)
1929 if dnstr not in self.bridgehead_list:
1930 self.bridgehead_list.append(dnstr)
1933 class RepsFromTo(object):
1934 """Class encapsulation of the NDR repsFromToBlob.
1936 Removes the necessity of external code having to
1937 understand about other_info or manipulation of
1940 def __init__(self, nc_dnstr=None, ndr_blob=None):
1942 self.__dict__['to_be_deleted'] = False
1943 self.__dict__['nc_dnstr'] = nc_dnstr
1944 self.__dict__['update_flags'] = 0x0
1945 # XXX the following sounds dubious and/or better solved
1946 # elsewhere, but lets leave it for now. In particular, there
1947 # seems to be no reason for all the non-ndr generated
1948 # attributes to be handled in the round about way (e.g.
1949 # self.__dict__['to_be_deleted'] = False above). On the other
1950 # hand, it all seems to work. Hooray! Hands off!.
1954 # There is a very subtle bug here with python
1955 # and our NDR code. If you assign directly to
1956 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1957 # then a proper python GC reference count is not
1960 # To work around this we maintain an internal
1961 # reference to "dns_name(x)" and "other_info" elements
1962 # of repsFromToBlob. This internal reference
1963 # is hidden within this class but it is why you
1964 # see statements like this below:
1966 # self.__dict__['ndr_blob'].ctr.other_info = \
1967 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1969 # That would appear to be a redundant assignment but
1970 # it is necessary to hold a proper python GC reference
1972 if ndr_blob is None:
1973 self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
1974 self.__dict__['ndr_blob'].version = 0x1
1975 self.__dict__['dns_name1'] = None
1976 self.__dict__['dns_name2'] = None
1978 self.__dict__['ndr_blob'].ctr.other_info = \
1979 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1982 self.__dict__['ndr_blob'] = ndr_blob
1983 self.__dict__['other_info'] = ndr_blob.ctr.other_info
1985 if ndr_blob.version == 0x1:
1986 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
1987 self.__dict__['dns_name2'] = None
1989 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
1990 self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
1993 '''Debug dump string output of class'''
1995 text = "%s:" % self.__class__.__name__
1996 text += "\n\tdnstr=%s" % self.nc_dnstr
1997 text += "\n\tupdate_flags=0x%X" % self.update_flags
1998 text += "\n\tversion=%d" % self.version
1999 text += "\n\tsource_dsa_obj_guid=%s" % self.source_dsa_obj_guid
2000 text += ("\n\tsource_dsa_invocation_id=%s" %
2001 self.source_dsa_invocation_id)
2002 text += "\n\ttransport_guid=%s" % self.transport_guid
2003 text += "\n\treplica_flags=0x%X" % self.replica_flags
2004 text += ("\n\tconsecutive_sync_failures=%d" %
2005 self.consecutive_sync_failures)
2006 text += "\n\tlast_success=%s" % self.last_success
2007 text += "\n\tlast_attempt=%s" % self.last_attempt
2008 text += "\n\tdns_name1=%s" % self.dns_name1
2009 text += "\n\tdns_name2=%s" % self.dns_name2
2010 text += "\n\tschedule[ "
2011 for slot in self.schedule:
2012 text += "0x%X " % slot
2017 def __setattr__(self, item, value):
2018 """Set an attribute and chyange update flag.
2020 Be aware that setting any RepsFromTo attribute will set the
2021 drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS update flag.
2023 if item in ['schedule', 'replica_flags', 'transport_guid',
2024 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2025 'consecutive_sync_failures', 'last_success',
2028 if item in ['replica_flags']:
2029 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
2030 elif item in ['schedule']:
2031 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
2033 setattr(self.__dict__['ndr_blob'].ctr, item, value)
2035 elif item in ['dns_name1']:
2036 self.__dict__['dns_name1'] = value
2038 if self.__dict__['ndr_blob'].version == 0x1:
2039 self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
2040 self.__dict__['dns_name1']
2042 self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
2043 self.__dict__['dns_name1']
2045 elif item in ['dns_name2']:
2046 self.__dict__['dns_name2'] = value
2048 if self.__dict__['ndr_blob'].version == 0x1:
2049 raise AttributeError(item)
2051 self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
2052 self.__dict__['dns_name2']
2054 elif item in ['nc_dnstr']:
2055 self.__dict__['nc_dnstr'] = value
2057 elif item in ['to_be_deleted']:
2058 self.__dict__['to_be_deleted'] = value
2060 elif item in ['version']:
2061 raise AttributeError("Attempt to set readonly attribute %s" % item)
2063 raise AttributeError("Unknown attribute %s" % item)
2065 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
2067 def __getattr__(self, item):
2068 """Overload of RepsFromTo attribute retrieval.
2070 Allows external code to ignore substructures within the blob
2072 if item in ['schedule', 'replica_flags', 'transport_guid',
2073 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2074 'consecutive_sync_failures', 'last_success',
2076 return getattr(self.__dict__['ndr_blob'].ctr, item)
2078 elif item in ['version']:
2079 return self.__dict__['ndr_blob'].version
2081 elif item in ['dns_name1']:
2082 if self.__dict__['ndr_blob'].version == 0x1:
2083 return self.__dict__['ndr_blob'].ctr.other_info.dns_name
2085 return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
2087 elif item in ['dns_name2']:
2088 if self.__dict__['ndr_blob'].version == 0x1:
2089 raise AttributeError(item)
2091 return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
2093 elif item in ['to_be_deleted']:
2094 return self.__dict__['to_be_deleted']
2096 elif item in ['nc_dnstr']:
2097 return self.__dict__['nc_dnstr']
2099 elif item in ['update_flags']:
2100 return self.__dict__['update_flags']
2102 raise AttributeError("Unknown attribute %s" % item)
2104 def is_modified(self):
2105 return (self.update_flags != 0x0)
2107 def set_unmodified(self):
2108 self.__dict__['update_flags'] = 0x0
2111 class SiteLink(object):
2112 """Class defines a site link found under sites
2115 def __init__(self, dnstr):
2118 self.system_flags = 0
2120 self.schedule = None
2121 self.interval = None
2125 '''Debug dump string output of Transport object'''
2127 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
2128 text = text + "\n\toptions=%d" % self.options
2129 text = text + "\n\tsystem_flags=%d" % self.system_flags
2130 text = text + "\n\tcost=%d" % self.cost
2131 text = text + "\n\tinterval=%s" % self.interval
2133 if self.schedule is not None:
2134 text += "\n\tschedule.size=%s" % self.schedule.size
2135 text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
2136 text += ("\n\tschedule.numberOfSchedules=%s" %
2137 self.schedule.numberOfSchedules)
2139 for i, header in enumerate(self.schedule.headerArray):
2140 text += ("\n\tschedule.headerArray[%d].type=%d" %
2142 text += ("\n\tschedule.headerArray[%d].offset=%d" %
2144 text = text + "\n\tschedule.dataArray[%d].slots[ " % i
2145 for slot in self.schedule.dataArray[i].slots:
2146 text = text + "0x%X " % slot
2149 for dnstr in self.site_list:
2150 text = text + "\n\tsite_list=%s" % dnstr
2153 def load_sitelink(self, samdb):
2154 """Given a siteLink object with an prior initialization
2155 for the object's DN, search for the DN and load attributes
2165 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
2166 attrs=attrs, controls=['extended_dn:0'])
2168 except ldb.LdbError as e19:
2169 (enum, estr) = e19.args
2170 raise KCCError("Unable to find SiteLink for (%s) - (%s)" %
2175 if "options" in msg:
2176 self.options = int(msg["options"][0])
2178 if "systemFlags" in msg:
2179 self.system_flags = int(msg["systemFlags"][0])
2182 self.cost = int(msg["cost"][0])
2184 if "replInterval" in msg:
2185 self.interval = int(msg["replInterval"][0])
2187 if "siteList" in msg:
2188 for value in msg["siteList"]:
2189 dsdn = dsdb_Dn(samdb, value)
2190 guid = misc.GUID(dsdn.dn.get_extended_component('GUID'))
2191 if guid not in self.site_list:
2192 self.site_list.append(guid)
2194 if "schedule" in msg:
2195 self.schedule = ndr_unpack(drsblobs.schedule, value)
2197 self.schedule = new_connection_schedule()
2200 class KCCFailedObject(object):
2201 def __init__(self, uuid, failure_count, time_first_failure,
2202 last_result, dns_name):
2204 self.failure_count = failure_count
2205 self.time_first_failure = time_first_failure
2206 self.last_result = last_result
2207 self.dns_name = dns_name
2210 ##################################################
2211 # Global Functions and Variables
2212 ##################################################
2214 def get_dsa_config_rep(dsa):
2215 # Find configuration NC replica for the DSA
2216 for c_rep in dsa.current_rep_table.values():
2217 if c_rep.is_config():
2220 raise KCCError("Unable to find config NC replica for (%s)" %
2224 def sort_dsa_by_guid(dsa1, dsa2):
2225 "use ndr_pack for GUID comparison, as appears correct in some places"""
2226 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
2229 def new_connection_schedule():
2230 """Create a default schedule for an NTDSConnection or Sitelink. This
2231 is packed differently from the repltimes schedule used elsewhere
2232 in KCC (where the 168 nibbles are packed into 84 bytes).
2234 # 168 byte instances of the 0x01 value. The low order 4 bits
2235 # of the byte equate to 15 minute intervals within a single hour.
2236 # There are 168 bytes because there are 168 hours in a full week
2237 # Effectively we are saying to perform replication at the end of
2238 # each hour of the week
2239 schedule = drsblobs.schedule()
2242 schedule.bandwidth = 0
2243 schedule.numberOfSchedules = 1
2245 header = drsblobs.scheduleHeader()
2249 schedule.headerArray = [header]
2251 data = drsblobs.scheduleSlots()
2252 data.slots = [0x01] * 168
2254 schedule.dataArray = [data]
2258 ##################################################
2260 ##################################################
2262 def uncovered_sites_to_cover(samdb, site_name):
2264 Discover which sites have no DCs and whose lowest single-hop cost
2265 distance for any link attached to that site is linked to the site supplied.
2267 We compare the lowest cost of your single-hop link to this site to all of
2268 those available (if it exists). This means that a lower ranked siteLink
2269 with only the uncovered site can trump any available links (but this can
2270 only be done with specific, poorly enacted user configuration).
2272 If the site is connected to more than one other site with the same
2273 siteLink, only the largest site (failing that sorted alphabetically)
2274 creates the DNS records.
2276 :param samdb database
2277 :param site_name origin site (with a DC)
2279 :return a list of sites this site should be covering (for DNS)
2283 server_res = samdb.search(base=samdb.get_config_basedn(),
2284 scope=ldb.SCOPE_SUBTREE,
2285 expression="(&(objectClass=server)"
2286 "(serverReference=*))")
2288 site_res = samdb.search(base=samdb.get_config_basedn(),
2289 scope=ldb.SCOPE_SUBTREE,
2290 expression="(objectClass=site)")
2292 sites_in_use = Counter()
2295 # Assume server is of form DC,Servers,Site-ABCD because of schema
2296 for msg in server_res:
2297 site_dn = msg.dn.parent().parent()
2298 sites_in_use[site_dn.canonical_str()] += 1
2300 if site_dn.get_rdn_value().lower() == site_name.lower():
2303 if len(sites_in_use) != len(site_res):
2304 # There is a possible uncovered site
2305 sites_uncovered = []
2307 for msg in site_res:
2308 if msg.dn.canonical_str() not in sites_in_use:
2309 sites_uncovered.append(msg)
2311 own_site_dn = "CN={},CN=Sites,{}".format(
2312 ldb.binary_encode(site_name),
2313 ldb.binary_encode(str(samdb.get_config_basedn()))
2316 for site in sites_uncovered:
2317 encoded_dn = ldb.binary_encode(str(site.dn))
2319 # Get a sorted list of all siteLinks featuring the uncovered site
2320 link_res1 = samdb.search(base=samdb.get_config_basedn(),
2321 scope=ldb.SCOPE_SUBTREE, attrs=["cost"],
2322 expression="(&(objectClass=siteLink)"
2323 "(siteList={}))".format(encoded_dn),
2324 controls=["server_sort:1:0:cost"])
2326 # Get a sorted list of all siteLinks connecting this an the
2328 link_res2 = samdb.search(base=samdb.get_config_basedn(),
2329 scope=ldb.SCOPE_SUBTREE,
2330 attrs=["cost", "siteList"],
2331 expression="(&(objectClass=siteLink)"
2332 "(siteList={})(siteList={}))".format(
2335 controls=["server_sort:1:0:cost"])
2337 # Add to list if your link is equal in cost to lowest cost link
2338 if len(link_res1) > 0 and len(link_res2) > 0:
2339 cost1 = int(link_res1[0]['cost'][0])
2340 cost2 = int(link_res2[0]['cost'][0])
2342 # Own siteLink must match the lowest cost link
2346 # In a siteLink with more than 2 sites attached, only pick the
2347 # largest site, and if there are multiple, the earliest
2350 for site_val in link_res2[0]['siteList']:
2351 site_dn = ldb.Dn(samdb, str(site_val))
2352 site_dn_str = site_dn.canonical_str()
2353 site_rdn = site_dn.get_rdn_value().lower()
2354 if sites_in_use[site_dn_str] > dc_count:
2357 elif (sites_in_use[site_dn_str] == dc_count and
2358 site_rdn < site_name.lower()):
2363 site_cover_rdn = site.dn.get_rdn_value()
2364 sites_to_cover.append(site_cover_rdn.lower())
2366 return sites_to_cover