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.decode('utf8'))
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].decode('utf8'))
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].decode('utf8'))
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.decode('utf8'))
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
1585 self.rw_dsa_table.values(),
1586 key=lambda dsa: ndr_pack(dsa.dsa_guid))
1588 # double word number of 100 nanosecond intervals since 1600s
1590 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1591 # if o!interSiteTopologyFailover is 0 or has no value.
1593 # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1594 # so it appears we have to turn f into the same interval
1596 # interSiteTopologyFailover (if set) appears to be in minutes
1597 # so we'll need to convert to senconds and then 100 nanosecond
1599 # XXX [MS-ADTS] 6.2.2.3.1 says it is seconds, not minutes.
1601 # 10,000,000 is number of 100 nanosecond intervals in a second
1602 if self.site_topo_failover == 0:
1603 f = 2 * 60 * 60 * 10000000
1605 f = self.site_topo_failover * 60 * 10000000
1607 # Let o be the site settings object for the site of the local
1608 # DC, or NULL if no such o exists.
1609 d_dsa = self.dsa_table.get(self.site_topo_generator)
1611 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1612 # If o != NULL and o!interSiteTopologyGenerator is not the
1613 # nTDSDSA object for the local DC and
1614 # o!interSiteTopologyGenerator is an element dj of sequence D:
1616 if d_dsa is not None and d_dsa is not mydsa:
1617 # From MS-ADTS 6.2.2.3.1 ISTG Selection:
1618 # Let c be the cursor in the replUpToDateVector variable
1619 # associated with the NC replica of the config NC such
1620 # that c.uuidDsa = dj!invocationId. If no such c exists
1621 # (No evidence of replication from current ITSG):
1625 # Else if the current time < c.timeLastSyncSuccess - f
1626 # (Evidence of time sync problem on current ISTG):
1630 # Else (Evidence of replication from current ITSG):
1632 # Let t = c.timeLastSyncSuccess.
1634 # last_success appears to be a double word containing
1635 # number of 100 nanosecond intervals since the 1600s
1636 j_idx = D_sort.index(d_dsa)
1639 for cursor in c_rep.rep_replUpToDateVector_cursors:
1640 if d_dsa.dsa_ivid == cursor.source_dsa_invocation_id:
1648 #XXX doc says current time < c.timeLastSyncSuccess - f
1649 # which is true only if f is negative or clocks are wrong.
1650 # f is not negative in the default case (2 hours).
1651 elif self.nt_now - cursor.last_sync_success > f:
1656 t_time = cursor.last_sync_success
1658 # Otherwise (Nominate local DC as ISTG):
1659 # Let i be the integer such that di is the nTDSDSA
1660 # object for the local DC.
1661 # Let t = the current time.
1663 i_idx = D_sort.index(mydsa)
1664 t_time = self.nt_now
1666 # Compute a function that maintains the current ISTG if
1667 # it is alive, cycles through other candidates if not.
1669 # Let k be the integer (i + ((current time - t) /
1670 # o!interSiteTopologyFailover)) MOD |D|.
1672 # Note: We don't want to divide by zero here so they must
1673 # have meant "f" instead of "o!interSiteTopologyFailover"
1674 k_idx = (i_idx + ((self.nt_now - t_time) // f)) % len(D_sort)
1676 # The local writable DC acts as an ISTG for its site if and
1677 # only if dk is the nTDSDSA object for the local DC. If the
1678 # local DC does not act as an ISTG, the KCC skips the
1679 # remainder of this task.
1680 d_dsa = D_sort[k_idx]
1681 d_dsa.dsa_is_istg = True
1683 # Update if we are the ISTG, otherwise return
1684 if d_dsa is not mydsa:
1688 if self.site_topo_generator == mydsa.dsa_dnstr:
1691 self.site_topo_generator = mydsa.dsa_dnstr
1693 # If readonly database then do not perform a
1698 # Perform update to the samdb
1699 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1702 m.dn = ldb.Dn(samdb, ssdn)
1704 m["interSiteTopologyGenerator"] = \
1705 ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
1706 "interSiteTopologyGenerator")
1710 except ldb.LdbError as estr:
1712 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1716 def is_intrasite_topology_disabled(self):
1717 '''Returns True if intra-site topology is disabled for site'''
1718 return (self.site_options &
1719 dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0
1721 def is_intersite_topology_disabled(self):
1722 '''Returns True if inter-site topology is disabled for site'''
1723 return ((self.site_options &
1724 dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED)
1727 def is_random_bridgehead_disabled(self):
1728 '''Returns True if selection of random bridgehead is disabled'''
1729 return (self.site_options &
1730 dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0
1732 def is_detect_stale_disabled(self):
1733 '''Returns True if detect stale is disabled for site'''
1734 return (self.site_options &
1735 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0
1737 def is_cleanup_ntdsconn_disabled(self):
1738 '''Returns True if NTDS Connection cleanup is disabled for site'''
1739 return (self.site_options &
1740 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0
1742 def same_site(self, dsa):
1743 '''Return True if dsa is in this site'''
1744 if self.get_dsa(dsa.dsa_dnstr):
1748 def is_rodc_site(self):
1749 if len(self.dsa_table) > 0 and len(self.rw_dsa_table) == 0:
1754 '''Debug dump string output of class'''
1755 text = "%s:" % self.__class__.__name__
1756 text = text + "\n\tdn=%s" % self.site_dnstr
1757 text = text + "\n\toptions=0x%X" % self.site_options
1758 text = text + "\n\ttopo_generator=%s" % self.site_topo_generator
1759 text = text + "\n\ttopo_failover=%d" % self.site_topo_failover
1760 for key, dsa in self.dsa_table.items():
1761 text = text + "\n%s" % dsa
1765 class GraphNode(object):
1766 """A graph node describing a set of edges that should be directed to it.
1768 Each edge is a connection for a particular naming context replica directed
1769 from another node in the forest to this node.
1772 def __init__(self, dsa_dnstr, max_node_edges):
1773 """Instantiate the graph node according to a DSA dn string
1775 :param max_node_edges: maximum number of edges that should ever
1776 be directed to the node
1778 self.max_edges = max_node_edges
1779 self.dsa_dnstr = dsa_dnstr
1783 text = "%s:" % self.__class__.__name__
1784 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
1785 text = text + "\n\tmax_edges=%d" % self.max_edges
1787 for i, edge in enumerate(self.edge_from):
1788 if isinstance(edge, str):
1789 text += "\n\tedge_from[%d]=%s" % (i, edge)
1793 def add_edge_from(self, from_dsa_dnstr):
1794 """Add an edge from the dsa to our graph nodes edge from list
1796 :param from_dsa_dnstr: the dsa that the edge emanates from
1798 assert isinstance(from_dsa_dnstr, str)
1800 # No edges from myself to myself
1801 if from_dsa_dnstr == self.dsa_dnstr:
1803 # Only one edge from a particular node
1804 if from_dsa_dnstr in self.edge_from:
1806 # Not too many edges
1807 if len(self.edge_from) >= self.max_edges:
1809 self.edge_from.append(from_dsa_dnstr)
1812 def add_edges_from_connections(self, dsa):
1813 """For each nTDSConnection object associated with a particular
1814 DSA, we test if it implies an edge to this graph node (i.e.
1815 the "fromServer" attribute). If it does then we add an
1816 edge from the server unless we are over the max edges for this
1819 :param dsa: dsa with a dnstr equivalent to his graph node
1821 for connect in dsa.connect_table.values():
1822 self.add_edge_from(connect.from_dnstr)
1824 def add_connections_from_edges(self, dsa, transport):
1825 """For each edge directed to this graph node, ensure there
1826 is a corresponding nTDSConnection object in the dsa.
1828 for edge_dnstr in self.edge_from:
1829 connections = dsa.get_connection_by_from_dnstr(edge_dnstr)
1831 # For each edge directed to the NC replica that
1832 # "should be present" on the local DC, the KCC determines
1833 # whether an object c exists such that:
1835 # c is a child of the DC's nTDSDSA object.
1836 # c.objectCategory = nTDSConnection
1838 # Given the NC replica ri from which the edge is directed,
1839 # c.fromServer is the dsname of the nTDSDSA object of
1840 # the DC on which ri "is present".
1842 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1845 for connect in connections:
1846 if connect.is_rodc_topology():
1853 # if no such object exists then the KCC adds an object
1854 # c with the following attributes
1856 # Generate a new dnstr for this nTDSConnection
1857 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1858 flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1859 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1861 dsa.new_connection(opt, flags, transport, edge_dnstr, None)
1863 def has_sufficient_edges(self):
1864 '''Return True if we have met the maximum "from edges" criteria'''
1865 if len(self.edge_from) >= self.max_edges:
1870 class Transport(object):
1871 """Class defines a Inter-site transport found under Sites
1874 def __init__(self, dnstr):
1879 self.address_attr = None
1880 self.bridgehead_list = []
1883 '''Debug dump string output of Transport object'''
1885 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
1886 text = text + "\n\tguid=%s" % str(self.guid)
1887 text = text + "\n\toptions=%d" % self.options
1888 text = text + "\n\taddress_attr=%s" % self.address_attr
1889 text = text + "\n\tname=%s" % self.name
1890 for dnstr in self.bridgehead_list:
1891 text = text + "\n\tbridgehead_list=%s" % dnstr
1895 def load_transport(self, samdb):
1896 """Given a Transport object with an prior initialization
1897 for the object's DN, search for the DN and load attributes
1900 attrs = ["objectGUID",
1903 "bridgeheadServerListBL",
1904 "transportAddressAttribute"]
1906 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
1909 except ldb.LdbError as e18:
1910 (enum, estr) = e18.args
1911 raise KCCError("Unable to find Transport for (%s) - (%s)" %
1915 self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
1916 msg["objectGUID"][0]))
1918 if "options" in msg:
1919 self.options = int(msg["options"][0])
1921 if "transportAddressAttribute" in msg:
1922 self.address_attr = str(msg["transportAddressAttribute"][0])
1925 self.name = str(msg["name"][0])
1927 if "bridgeheadServerListBL" in msg:
1928 for value in msg["bridgeheadServerListBL"]:
1929 dsdn = dsdb_Dn(samdb, value.decode('utf8'))
1930 dnstr = str(dsdn.dn)
1931 if dnstr not in self.bridgehead_list:
1932 self.bridgehead_list.append(dnstr)
1935 class RepsFromTo(object):
1936 """Class encapsulation of the NDR repsFromToBlob.
1938 Removes the necessity of external code having to
1939 understand about other_info or manipulation of
1942 def __init__(self, nc_dnstr=None, ndr_blob=None):
1944 self.__dict__['to_be_deleted'] = False
1945 self.__dict__['nc_dnstr'] = nc_dnstr
1946 self.__dict__['update_flags'] = 0x0
1947 # XXX the following sounds dubious and/or better solved
1948 # elsewhere, but lets leave it for now. In particular, there
1949 # seems to be no reason for all the non-ndr generated
1950 # attributes to be handled in the round about way (e.g.
1951 # self.__dict__['to_be_deleted'] = False above). On the other
1952 # hand, it all seems to work. Hooray! Hands off!.
1956 # There is a very subtle bug here with python
1957 # and our NDR code. If you assign directly to
1958 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1959 # then a proper python GC reference count is not
1962 # To work around this we maintain an internal
1963 # reference to "dns_name(x)" and "other_info" elements
1964 # of repsFromToBlob. This internal reference
1965 # is hidden within this class but it is why you
1966 # see statements like this below:
1968 # self.__dict__['ndr_blob'].ctr.other_info = \
1969 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1971 # That would appear to be a redundant assignment but
1972 # it is necessary to hold a proper python GC reference
1974 if ndr_blob is None:
1975 self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
1976 self.__dict__['ndr_blob'].version = 0x1
1977 self.__dict__['dns_name1'] = None
1978 self.__dict__['dns_name2'] = None
1980 self.__dict__['ndr_blob'].ctr.other_info = \
1981 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1984 self.__dict__['ndr_blob'] = ndr_blob
1985 self.__dict__['other_info'] = ndr_blob.ctr.other_info
1987 if ndr_blob.version == 0x1:
1988 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
1989 self.__dict__['dns_name2'] = None
1991 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
1992 self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
1995 '''Debug dump string output of class'''
1997 text = "%s:" % self.__class__.__name__
1998 text += "\n\tdnstr=%s" % self.nc_dnstr
1999 text += "\n\tupdate_flags=0x%X" % self.update_flags
2000 text += "\n\tversion=%d" % self.version
2001 text += "\n\tsource_dsa_obj_guid=%s" % self.source_dsa_obj_guid
2002 text += ("\n\tsource_dsa_invocation_id=%s" %
2003 self.source_dsa_invocation_id)
2004 text += "\n\ttransport_guid=%s" % self.transport_guid
2005 text += "\n\treplica_flags=0x%X" % self.replica_flags
2006 text += ("\n\tconsecutive_sync_failures=%d" %
2007 self.consecutive_sync_failures)
2008 text += "\n\tlast_success=%s" % self.last_success
2009 text += "\n\tlast_attempt=%s" % self.last_attempt
2010 text += "\n\tdns_name1=%s" % self.dns_name1
2011 text += "\n\tdns_name2=%s" % self.dns_name2
2012 text += "\n\tschedule[ "
2013 for slot in self.schedule:
2014 text += "0x%X " % slot
2019 def __setattr__(self, item, value):
2020 """Set an attribute and chyange update flag.
2022 Be aware that setting any RepsFromTo attribute will set the
2023 drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS update flag.
2025 if item in ['schedule', 'replica_flags', 'transport_guid',
2026 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2027 'consecutive_sync_failures', 'last_success',
2030 if item in ['replica_flags']:
2031 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
2032 elif item in ['schedule']:
2033 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
2035 setattr(self.__dict__['ndr_blob'].ctr, item, value)
2037 elif item in ['dns_name1']:
2038 self.__dict__['dns_name1'] = value
2040 if self.__dict__['ndr_blob'].version == 0x1:
2041 self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
2042 self.__dict__['dns_name1']
2044 self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
2045 self.__dict__['dns_name1']
2047 elif item in ['dns_name2']:
2048 self.__dict__['dns_name2'] = value
2050 if self.__dict__['ndr_blob'].version == 0x1:
2051 raise AttributeError(item)
2053 self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
2054 self.__dict__['dns_name2']
2056 elif item in ['nc_dnstr']:
2057 self.__dict__['nc_dnstr'] = value
2059 elif item in ['to_be_deleted']:
2060 self.__dict__['to_be_deleted'] = value
2062 elif item in ['version']:
2063 raise AttributeError("Attempt to set readonly attribute %s" % item)
2065 raise AttributeError("Unknown attribute %s" % item)
2067 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
2069 def __getattr__(self, item):
2070 """Overload of RepsFromTo attribute retrieval.
2072 Allows external code to ignore substructures within the blob
2074 if item in ['schedule', 'replica_flags', 'transport_guid',
2075 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2076 'consecutive_sync_failures', 'last_success',
2078 return getattr(self.__dict__['ndr_blob'].ctr, item)
2080 elif item in ['version']:
2081 return self.__dict__['ndr_blob'].version
2083 elif item in ['dns_name1']:
2084 if self.__dict__['ndr_blob'].version == 0x1:
2085 return self.__dict__['ndr_blob'].ctr.other_info.dns_name
2087 return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
2089 elif item in ['dns_name2']:
2090 if self.__dict__['ndr_blob'].version == 0x1:
2091 raise AttributeError(item)
2093 return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
2095 elif item in ['to_be_deleted']:
2096 return self.__dict__['to_be_deleted']
2098 elif item in ['nc_dnstr']:
2099 return self.__dict__['nc_dnstr']
2101 elif item in ['update_flags']:
2102 return self.__dict__['update_flags']
2104 raise AttributeError("Unknown attribute %s" % item)
2106 def is_modified(self):
2107 return (self.update_flags != 0x0)
2109 def set_unmodified(self):
2110 self.__dict__['update_flags'] = 0x0
2113 class SiteLink(object):
2114 """Class defines a site link found under sites
2117 def __init__(self, dnstr):
2120 self.system_flags = 0
2122 self.schedule = None
2123 self.interval = None
2127 '''Debug dump string output of Transport object'''
2129 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
2130 text = text + "\n\toptions=%d" % self.options
2131 text = text + "\n\tsystem_flags=%d" % self.system_flags
2132 text = text + "\n\tcost=%d" % self.cost
2133 text = text + "\n\tinterval=%s" % self.interval
2135 if self.schedule is not None:
2136 text += "\n\tschedule.size=%s" % self.schedule.size
2137 text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
2138 text += ("\n\tschedule.numberOfSchedules=%s" %
2139 self.schedule.numberOfSchedules)
2141 for i, header in enumerate(self.schedule.headerArray):
2142 text += ("\n\tschedule.headerArray[%d].type=%d" %
2144 text += ("\n\tschedule.headerArray[%d].offset=%d" %
2146 text = text + "\n\tschedule.dataArray[%d].slots[ " % i
2147 for slot in self.schedule.dataArray[i].slots:
2148 text = text + "0x%X " % slot
2151 for guid, dn in self.site_list:
2152 text = text + "\n\tsite_list=%s (%s)" % (guid, dn)
2155 def load_sitelink(self, samdb):
2156 """Given a siteLink object with an prior initialization
2157 for the object's DN, search for the DN and load attributes
2167 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
2168 attrs=attrs, controls=['extended_dn:0'])
2170 except ldb.LdbError as e19:
2171 (enum, estr) = e19.args
2172 raise KCCError("Unable to find SiteLink for (%s) - (%s)" %
2177 if "options" in msg:
2178 self.options = int(msg["options"][0])
2180 if "systemFlags" in msg:
2181 self.system_flags = int(msg["systemFlags"][0])
2184 self.cost = int(msg["cost"][0])
2186 if "replInterval" in msg:
2187 self.interval = int(msg["replInterval"][0])
2189 if "siteList" in msg:
2190 for value in msg["siteList"]:
2191 dsdn = dsdb_Dn(samdb, value.decode('utf8'))
2192 guid = misc.GUID(dsdn.dn.get_extended_component('GUID'))
2193 dnstr = str(dsdn.dn)
2194 if (guid, dnstr) not in self.site_list:
2195 self.site_list.append((guid, dnstr))
2197 if "schedule" in msg:
2198 self.schedule = ndr_unpack(drsblobs.schedule, value)
2200 self.schedule = new_connection_schedule()
2203 class KCCFailedObject(object):
2204 def __init__(self, uuid, failure_count, time_first_failure,
2205 last_result, dns_name):
2207 self.failure_count = failure_count
2208 self.time_first_failure = time_first_failure
2209 self.last_result = last_result
2210 self.dns_name = dns_name
2213 ##################################################
2214 # Global Functions and Variables
2215 ##################################################
2217 def get_dsa_config_rep(dsa):
2218 # Find configuration NC replica for the DSA
2219 for c_rep in dsa.current_rep_table.values():
2220 if c_rep.is_config():
2223 raise KCCError("Unable to find config NC replica for (%s)" %
2227 def new_connection_schedule():
2228 """Create a default schedule for an NTDSConnection or Sitelink. This
2229 is packed differently from the repltimes schedule used elsewhere
2230 in KCC (where the 168 nibbles are packed into 84 bytes).
2232 # 168 byte instances of the 0x01 value. The low order 4 bits
2233 # of the byte equate to 15 minute intervals within a single hour.
2234 # There are 168 bytes because there are 168 hours in a full week
2235 # Effectively we are saying to perform replication at the end of
2236 # each hour of the week
2237 schedule = drsblobs.schedule()
2240 schedule.bandwidth = 0
2241 schedule.numberOfSchedules = 1
2243 header = drsblobs.scheduleHeader()
2247 schedule.headerArray = [header]
2249 data = drsblobs.scheduleSlots()
2250 data.slots = [0x01] * 168
2252 schedule.dataArray = [data]
2256 ##################################################
2258 ##################################################
2260 def uncovered_sites_to_cover(samdb, site_name):
2262 Discover which sites have no DCs and whose lowest single-hop cost
2263 distance for any link attached to that site is linked to the site supplied.
2265 We compare the lowest cost of your single-hop link to this site to all of
2266 those available (if it exists). This means that a lower ranked siteLink
2267 with only the uncovered site can trump any available links (but this can
2268 only be done with specific, poorly enacted user configuration).
2270 If the site is connected to more than one other site with the same
2271 siteLink, only the largest site (failing that sorted alphabetically)
2272 creates the DNS records.
2274 :param samdb database
2275 :param site_name origin site (with a DC)
2277 :return a list of sites this site should be covering (for DNS)
2281 server_res = samdb.search(base=samdb.get_config_basedn(),
2282 scope=ldb.SCOPE_SUBTREE,
2283 expression="(&(objectClass=server)"
2284 "(serverReference=*))")
2286 site_res = samdb.search(base=samdb.get_config_basedn(),
2287 scope=ldb.SCOPE_SUBTREE,
2288 expression="(objectClass=site)")
2290 sites_in_use = Counter()
2293 # Assume server is of form DC,Servers,Site-ABCD because of schema
2294 for msg in server_res:
2295 site_dn = msg.dn.parent().parent()
2296 sites_in_use[site_dn.canonical_str()] += 1
2298 if site_dn.get_rdn_value().lower() == site_name.lower():
2301 if len(sites_in_use) != len(site_res):
2302 # There is a possible uncovered site
2303 sites_uncovered = []
2305 for msg in site_res:
2306 if msg.dn.canonical_str() not in sites_in_use:
2307 sites_uncovered.append(msg)
2309 own_site_dn = "CN={},CN=Sites,{}".format(
2310 ldb.binary_encode(site_name),
2311 ldb.binary_encode(str(samdb.get_config_basedn()))
2314 for site in sites_uncovered:
2315 encoded_dn = ldb.binary_encode(str(site.dn))
2317 # Get a sorted list of all siteLinks featuring the uncovered site
2318 link_res1 = samdb.search(base=samdb.get_config_basedn(),
2319 scope=ldb.SCOPE_SUBTREE, attrs=["cost"],
2320 expression="(&(objectClass=siteLink)"
2321 "(siteList={}))".format(encoded_dn),
2322 controls=["server_sort:1:0:cost"])
2324 # Get a sorted list of all siteLinks connecting this an the
2326 link_res2 = samdb.search(base=samdb.get_config_basedn(),
2327 scope=ldb.SCOPE_SUBTREE,
2328 attrs=["cost", "siteList"],
2329 expression="(&(objectClass=siteLink)"
2330 "(siteList={})(siteList={}))".format(
2333 controls=["server_sort:1:0:cost"])
2335 # Add to list if your link is equal in cost to lowest cost link
2336 if len(link_res1) > 0 and len(link_res2) > 0:
2337 cost1 = int(link_res1[0]['cost'][0])
2338 cost2 = int(link_res2[0]['cost'][0])
2340 # Own siteLink must match the lowest cost link
2344 # In a siteLink with more than 2 sites attached, only pick the
2345 # largest site, and if there are multiple, the earliest
2348 for site_val in link_res2[0]['siteList']:
2349 site_dn = ldb.Dn(samdb, str(site_val))
2350 site_dn_str = site_dn.canonical_str()
2351 site_rdn = site_dn.get_rdn_value().lower()
2352 if sites_in_use[site_dn_str] > dc_count:
2355 elif (sites_in_use[site_dn_str] == dc_count and
2356 site_rdn < site_name.lower()):
2361 site_cover_rdn = site.dn.get_rdn_value()
2362 sites_to_cover.append(site_cover_rdn.lower())
2364 return sites_to_cover