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)
45 # map the NCType enum to strings for debugging
46 nctype_lut = dict((v, k) for k, v in NCType.__dict__.items() if k[:2] != '__')
49 class NamingContext(object):
50 """Base class for a naming context.
52 Holds the DN, GUID, SID (if available) and type of the DN.
53 Subclasses may inherit from this and specialize
56 def __init__(self, nc_dnstr):
57 """Instantiate a NamingContext
59 :param nc_dnstr: NC dn string
61 self.nc_dnstr = nc_dnstr
64 self.nc_type = NCType.unknown
67 '''Debug dump string output of class'''
68 text = "%s:" % (self.__class__.__name__,)
69 text = text + "\n\tnc_dnstr=%s" % self.nc_dnstr
70 text = text + "\n\tnc_guid=%s" % str(self.nc_guid)
72 if self.nc_sid is None:
73 text = text + "\n\tnc_sid=<absent>"
75 text = text + "\n\tnc_sid=<present>"
77 text = text + "\n\tnc_type=%s (%s)" % (nctype_lut[self.nc_type],
81 def load_nc(self, samdb):
82 attrs = ["objectGUID",
85 res = samdb.search(base=self.nc_dnstr,
86 scope=ldb.SCOPE_BASE, attrs=attrs)
88 except ldb.LdbError as e:
90 raise KCCError("Unable to find naming context (%s) - (%s)" %
91 (self.nc_dnstr, estr))
93 if "objectGUID" in msg:
94 self.nc_guid = misc.GUID(samdb.schema_format_value("objectGUID",
95 msg["objectGUID"][0]))
96 if "objectSid" in msg:
97 self.nc_sid = msg["objectSid"][0]
99 assert self.nc_guid is not None
102 '''Return True if NC is config'''
103 assert self.nc_type != NCType.unknown
104 return self.nc_type == NCType.config
106 def identify_by_basedn(self, samdb):
107 """Given an NC object, identify what type is is thru
108 the samdb basedn strings and NC sid value
110 # Invoke loader to initialize guid and more
111 # importantly sid value (sid is used to identify
113 if self.nc_guid is None:
116 # We check against schema and config because they
117 # will be the same for all nTDSDSAs in the forest.
118 # That leaves the domain NCs which can be identified
119 # by sid and application NCs as the last identified
120 if self.nc_dnstr == str(samdb.get_schema_basedn()):
121 self.nc_type = NCType.schema
122 elif self.nc_dnstr == str(samdb.get_config_basedn()):
123 self.nc_type = NCType.config
124 elif self.nc_sid is not None:
125 self.nc_type = NCType.domain
127 self.nc_type = NCType.application
129 def identify_by_dsa_attr(self, samdb, attr):
130 """Given an NC which has been discovered thru the
131 nTDSDSA database object, determine what type of NC
132 it is (i.e. schema, config, domain, application) via
133 the use of the schema attribute under which the NC
136 :param attr: attr of nTDSDSA object where NC DN appears
138 # If the NC is listed under msDS-HasDomainNCs then
139 # this can only be a domain NC and it is our default
140 # domain for this dsa
141 if attr == "msDS-HasDomainNCs":
142 self.nc_type = NCType.domain
144 # If the NC is listed under hasPartialReplicaNCs
145 # this is only a domain NC
146 elif attr == "hasPartialReplicaNCs":
147 self.nc_type = NCType.domain
149 # NCs listed under hasMasterNCs are either
150 # default domain, schema, or config. We
151 # utilize the identify_by_basedn() to
153 elif attr == "hasMasterNCs":
154 self.identify_by_basedn(samdb)
156 # Still unknown (unlikely) but for completeness
157 # and for finally identifying application NCs
158 if self.nc_type == NCType.unknown:
159 self.identify_by_basedn(samdb)
162 class NCReplica(NamingContext):
163 """Naming context replica that is relative to a specific DSA.
165 This is a more specific form of NamingContext class (inheriting from that
166 class) and it identifies unique attributes of the DSA's replica for a NC.
169 def __init__(self, dsa, nc_dnstr):
170 """Instantiate a Naming Context Replica
172 :param dsa_guid: GUID of DSA where replica appears
173 :param nc_dnstr: NC dn string
175 self.rep_dsa_dnstr = dsa.dsa_dnstr
176 self.rep_dsa_guid = dsa.dsa_guid
177 self.rep_default = False # replica for DSA's default domain
178 self.rep_partial = False
180 self.rep_instantiated_flags = 0
182 self.rep_fsmo_role_owner = None
185 self.rep_repsFrom = []
190 # The (is present) test is a combination of being
191 # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or
192 # hasPartialReplicaNCs) as well as its replica flags found
193 # thru the msDS-HasInstantiatedNCs. If the NC replica meets
194 # the first enumeration test then this flag is set true
195 self.rep_present_criteria_one = False
197 # Call my super class we inherited from
198 NamingContext.__init__(self, nc_dnstr)
201 '''Debug dump string output of class'''
202 text = "%s:" % self.__class__.__name__
203 text = text + "\n\tdsa_dnstr=%s" % self.rep_dsa_dnstr
204 text = text + "\n\tdsa_guid=%s" % self.rep_dsa_guid
205 text = text + "\n\tdefault=%s" % self.rep_default
206 text = text + "\n\tro=%s" % self.rep_ro
207 text = text + "\n\tpartial=%s" % self.rep_partial
208 text = text + "\n\tpresent=%s" % self.is_present()
209 text = text + "\n\tfsmo_role_owner=%s" % self.rep_fsmo_role_owner
211 for rep in self.rep_repsFrom:
212 text = text + "\n%s" % rep
214 for rep in self.rep_repsTo:
215 text = text + "\n%s" % rep
217 return "%s\n%s" % (NamingContext.__str__(self), text)
219 def set_instantiated_flags(self, flags=0):
220 '''Set or clear NC replica instantiated flags'''
221 self.rep_instantiated_flags = flags
223 def identify_by_dsa_attr(self, samdb, attr):
224 """Given an NC which has been discovered thru the
225 nTDSDSA database object, determine what type of NC
226 replica it is (i.e. partial, read only, default)
228 :param attr: attr of nTDSDSA object where NC DN appears
230 # If the NC was found under hasPartialReplicaNCs
231 # then a partial replica at this dsa
232 if attr == "hasPartialReplicaNCs":
233 self.rep_partial = True
234 self.rep_present_criteria_one = True
236 # If the NC is listed under msDS-HasDomainNCs then
237 # this can only be a domain NC and it is the DSA's
239 elif attr == "msDS-HasDomainNCs":
240 self.rep_default = True
242 # NCs listed under hasMasterNCs are either
243 # default domain, schema, or config. We check
244 # against schema and config because they will be
245 # the same for all nTDSDSAs in the forest. That
246 # leaves the default domain NC remaining which
247 # may be different for each nTDSDSAs (and thus
248 # we don't compare agains this samdb's default
250 elif attr == "hasMasterNCs":
251 self.rep_present_criteria_one = True
253 if self.nc_dnstr != str(samdb.get_schema_basedn()) and \
254 self.nc_dnstr != str(samdb.get_config_basedn()):
255 self.rep_default = True
258 elif attr == "msDS-hasFullReplicaNCs":
259 self.rep_present_criteria_one = True
263 elif attr == "msDS-hasMasterNCs":
264 self.rep_present_criteria_one = True
267 # Now use this DSA attribute to identify the naming
268 # context type by calling the super class method
270 NamingContext.identify_by_dsa_attr(self, samdb, attr)
272 def is_default(self):
273 """Whether this is a default domain for the dsa that this NC appears on
275 return self.rep_default
278 '''Return True if NC replica is read only'''
281 def is_partial(self):
282 '''Return True if NC replica is partial'''
283 return self.rep_partial
285 def is_present(self):
286 """Given an NC replica which has been discovered thru the
287 nTDSDSA database object and populated with replica flags
288 from the msDS-HasInstantiatedNCs; return whether the NC
289 replica is present (true) or if the IT_NC_GOING flag is
290 set then the NC replica is not present (false)
292 if self.rep_present_criteria_one and \
293 self.rep_instantiated_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0:
297 def load_repsFrom(self, samdb):
298 """Given an NC replica which has been discovered thru the nTDSDSA
299 database object, load the repsFrom attribute for the local replica.
300 held by my dsa. The repsFrom attribute is not replicated so this
301 attribute is relative only to the local DSA that the samdb exists on
304 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
307 except ldb.LdbError as e1:
308 (enum, estr) = e1.args
309 raise KCCError("Unable to find NC for (%s) - (%s)" %
310 (self.nc_dnstr, estr))
314 # Possibly no repsFrom if this is a singleton DC
315 if "repsFrom" in msg:
316 for value in msg["repsFrom"]:
317 rep = RepsFromTo(self.nc_dnstr,
318 ndr_unpack(drsblobs.repsFromToBlob, value))
319 self.rep_repsFrom.append(rep)
321 def commit_repsFrom(self, samdb, ro=False):
322 """Commit repsFrom to the database"""
324 # XXX - This is not truly correct according to the MS-TECH
325 # docs. To commit a repsFrom we should be using RPCs
326 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
327 # IDL_DRSReplicaDel to affect a repsFrom change.
329 # Those RPCs are missing in samba, so I'll have to
330 # implement them to get this to more accurately
331 # reflect the reference docs. As of right now this
332 # commit to the database will work as its what the
338 for repsFrom in self.rep_repsFrom:
340 # Leave out any to be deleted from
341 # replacement list. Build a list
342 # of to be deleted reps which we will
343 # remove from rep_repsFrom list below
344 if repsFrom.to_be_deleted:
345 delreps.append(repsFrom)
349 if repsFrom.is_modified():
350 repsFrom.set_unmodified()
353 # current (unmodified) elements also get
354 # appended here but no changes will occur
355 # unless something is "to be modified" or
357 newreps.append(ndr_pack(repsFrom.ndr_blob))
359 # Now delete these from our list of rep_repsFrom
360 for repsFrom in delreps:
361 self.rep_repsFrom.remove(repsFrom)
364 # Nothing to do if no reps have been modified or
365 # need to be deleted or input option has informed
366 # us to be "readonly" (ro). Leave database
372 m.dn = ldb.Dn(samdb, self.nc_dnstr)
375 ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsFrom")
380 except ldb.LdbError as estr:
381 raise KCCError("Could not set repsFrom for (%s) - (%s)" %
382 (self.nc_dnstr, estr))
384 def load_replUpToDateVector(self, samdb):
385 """Given an NC replica which has been discovered thru the nTDSDSA
386 database object, load the replUpToDateVector attribute for the
387 local replica. held by my dsa. The replUpToDateVector
388 attribute is not replicated so this attribute is relative only
389 to the local DSA that the samdb exists on
393 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
394 attrs=["replUpToDateVector"])
396 except ldb.LdbError as e2:
397 (enum, estr) = e2.args
398 raise KCCError("Unable to find NC for (%s) - (%s)" %
399 (self.nc_dnstr, estr))
403 # Possibly no replUpToDateVector if this is a singleton DC
404 if "replUpToDateVector" in msg:
405 value = msg["replUpToDateVector"][0]
406 blob = ndr_unpack(drsblobs.replUpToDateVectorBlob,
408 if blob.version != 2:
409 # Samba only generates version 2, and this runs locally
410 raise AttributeError("Unexpected replUpToDateVector version %d"
413 self.rep_replUpToDateVector_cursors = blob.ctr.cursors
415 self.rep_replUpToDateVector_cursors = []
417 def dumpstr_to_be_deleted(self):
418 return '\n'.join(str(x) for x in self.rep_repsFrom if x.to_be_deleted)
420 def dumpstr_to_be_modified(self):
421 return '\n'.join(str(x) for x in self.rep_repsFrom if x.is_modified())
423 def load_fsmo_roles(self, samdb):
424 """Given an NC replica which has been discovered thru the nTDSDSA
425 database object, load the fSMORoleOwner attribute.
428 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
429 attrs=["fSMORoleOwner"])
431 except ldb.LdbError as e3:
432 (enum, estr) = e3.args
433 raise KCCError("Unable to find NC for (%s) - (%s)" %
434 (self.nc_dnstr, estr))
438 # Possibly no fSMORoleOwner
439 if "fSMORoleOwner" in msg:
440 self.rep_fsmo_role_owner = msg["fSMORoleOwner"]
442 def is_fsmo_role_owner(self, dsa_dnstr):
443 if self.rep_fsmo_role_owner is not None and \
444 self.rep_fsmo_role_owner == dsa_dnstr:
448 def load_repsTo(self, samdb):
449 """Given an NC replica which has been discovered thru the nTDSDSA
450 database object, load the repsTo attribute for the local replica.
451 held by my dsa. The repsTo attribute is not replicated so this
452 attribute is relative only to the local DSA that the samdb exists on
454 This is responsible for push replication, not scheduled pull
455 replication. Not to be confused for repsFrom.
458 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
461 except ldb.LdbError as e4:
462 (enum, estr) = e4.args
463 raise KCCError("Unable to find NC for (%s) - (%s)" %
464 (self.nc_dnstr, estr))
468 # Possibly no repsTo if this is a singleton DC
470 for value in msg["repsTo"]:
471 rep = RepsFromTo(self.nc_dnstr,
472 ndr_unpack(drsblobs.repsFromToBlob, value))
473 self.rep_repsTo.append(rep)
475 def commit_repsTo(self, samdb, ro=False):
476 """Commit repsTo to the database"""
478 # XXX - This is not truly correct according to the MS-TECH
479 # docs. To commit a repsTo we should be using RPCs
480 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
481 # IDL_DRSReplicaDel to affect a repsTo change.
483 # Those RPCs are missing in samba, so I'll have to
484 # implement them to get this to more accurately
485 # reflect the reference docs. As of right now this
486 # commit to the database will work as its what the
492 for repsTo in self.rep_repsTo:
494 # Leave out any to be deleted from
495 # replacement list. Build a list
496 # of to be deleted reps which we will
497 # remove from rep_repsTo list below
498 if repsTo.to_be_deleted:
499 delreps.append(repsTo)
503 if repsTo.is_modified():
504 repsTo.set_unmodified()
507 # current (unmodified) elements also get
508 # appended here but no changes will occur
509 # unless something is "to be modified" or
511 newreps.append(ndr_pack(repsTo.ndr_blob))
513 # Now delete these from our list of rep_repsTo
514 for repsTo in delreps:
515 self.rep_repsTo.remove(repsTo)
518 # Nothing to do if no reps have been modified or
519 # need to be deleted or input option has informed
520 # us to be "readonly" (ro). Leave database
526 m.dn = ldb.Dn(samdb, self.nc_dnstr)
529 ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsTo")
534 except ldb.LdbError as estr:
535 raise KCCError("Could not set repsTo for (%s) - (%s)" %
536 (self.nc_dnstr, estr))
539 class DirectoryServiceAgent(object):
541 def __init__(self, dsa_dnstr):
542 """Initialize DSA class.
544 Class is subsequently fully populated by calling the load_dsa() method
546 :param dsa_dnstr: DN of the nTDSDSA
548 self.dsa_dnstr = dsa_dnstr
551 self.dsa_is_ro = False
552 self.dsa_is_istg = False
554 self.dsa_behavior = 0
555 self.default_dnstr = None # default domain dn string for dsa
557 # NCReplicas for this dsa that are "present"
558 # Indexed by DN string of naming context
559 self.current_rep_table = {}
561 # NCReplicas for this dsa that "should be present"
562 # Indexed by DN string of naming context
563 self.needed_rep_table = {}
565 # NTDSConnections for this dsa. These are current
566 # valid connections that are committed or pending a commit
567 # in the database. Indexed by DN string of connection
568 self.connect_table = {}
571 '''Debug dump string output of class'''
573 text = "%s:" % self.__class__.__name__
574 if self.dsa_dnstr is not None:
575 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
576 if self.dsa_guid is not None:
577 text = text + "\n\tdsa_guid=%s" % str(self.dsa_guid)
578 if self.dsa_ivid is not None:
579 text = text + "\n\tdsa_ivid=%s" % str(self.dsa_ivid)
581 text = text + "\n\tro=%s" % self.is_ro()
582 text = text + "\n\tgc=%s" % self.is_gc()
583 text = text + "\n\tistg=%s" % self.is_istg()
585 text = text + "\ncurrent_replica_table:"
586 text = text + "\n%s" % self.dumpstr_current_replica_table()
587 text = text + "\nneeded_replica_table:"
588 text = text + "\n%s" % self.dumpstr_needed_replica_table()
589 text = text + "\nconnect_table:"
590 text = text + "\n%s" % self.dumpstr_connect_table()
594 def get_current_replica(self, nc_dnstr):
595 return self.current_rep_table.get(nc_dnstr)
598 '''Returns True if dsa is intersite topology generator for it's site'''
599 # The KCC on an RODC always acts as an ISTG for itself
600 return self.dsa_is_istg or self.dsa_is_ro
603 '''Returns True if dsa a read only domain controller'''
604 return self.dsa_is_ro
607 '''Returns True if dsa hosts a global catalog'''
608 if (self.options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
612 def is_minimum_behavior(self, version):
613 """Is dsa at minimum windows level greater than or equal to (version)
615 :param version: Windows version to test against
616 (e.g. DS_DOMAIN_FUNCTION_2008)
618 if self.dsa_behavior >= version:
622 def is_translate_ntdsconn_disabled(self):
623 """Whether this allows NTDSConnection translation in its options."""
624 if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0:
628 def get_rep_tables(self):
629 """Return DSA current and needed replica tables
631 return self.current_rep_table, self.needed_rep_table
633 def get_parent_dnstr(self):
634 """Get the parent DN string of this object."""
635 head, sep, tail = self.dsa_dnstr.partition(',')
638 def load_dsa(self, samdb):
639 """Load a DSA from the samdb.
641 Prior initialization has given us the DN of the DSA that we are to
642 load. This method initializes all other attributes, including loading
643 the NC replica table for this DSA.
645 attrs = ["objectGUID",
649 "msDS-Behavior-Version"]
651 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
654 except ldb.LdbError as e5:
655 (enum, estr) = e5.args
656 raise KCCError("Unable to find nTDSDSA for (%s) - (%s)" %
657 (self.dsa_dnstr, estr))
660 self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID",
661 msg["objectGUID"][0]))
663 # RODCs don't originate changes and thus have no invocationId,
664 # therefore we must check for existence first
665 if "invocationId" in msg:
666 self.dsa_ivid = misc.GUID(samdb.schema_format_value("objectGUID",
667 msg["invocationId"][0]))
670 self.options = int(msg["options"][0])
672 if "msDS-isRODC" in msg and str(msg["msDS-isRODC"][0]) == "TRUE":
673 self.dsa_is_ro = True
675 self.dsa_is_ro = False
677 if "msDS-Behavior-Version" in msg:
678 self.dsa_behavior = int(msg['msDS-Behavior-Version'][0])
680 # Load the NC replicas that are enumerated on this dsa
681 self.load_current_replica_table(samdb)
683 # Load the nTDSConnection that are enumerated on this dsa
684 self.load_connection_table(samdb)
686 def load_current_replica_table(self, samdb):
687 """Method to load the NC replica's listed for DSA object.
689 This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs,
690 hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, and
691 msDS-HasInstantiatedNCs) to determine complete list of NC replicas that
692 are enumerated for the DSA. Once a NC replica is loaded it is
693 identified (schema, config, etc) and the other replica attributes
694 (partial, ro, etc) are determined.
696 :param samdb: database to query for DSA replica list
699 # not RODC - default, config, schema (old style)
701 # not RODC - default, config, schema, app NCs
703 # domain NC partial replicas
704 "hasPartialReplicaNCs",
707 # RODC only - default, config, schema, app NCs
708 "msDS-hasFullReplicaNCs",
709 # Identifies if replica is coming, going, or stable
710 "msDS-HasInstantiatedNCs"
713 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
716 except ldb.LdbError as e6:
717 (enum, estr) = e6.args
718 raise KCCError("Unable to find nTDSDSA NCs for (%s) - (%s)" %
719 (self.dsa_dnstr, estr))
721 # The table of NCs for the dsa we are searching
724 # We should get one response to our query here for
725 # the ntds that we requested
728 # Our response will contain a number of elements including
729 # the dn of the dsa as well as elements for each
730 # attribute (e.g. hasMasterNCs). Each of these elements
731 # is a dictonary list which we retrieve the keys for and
732 # then iterate over them
733 for k in res[0].keys():
737 # For each attribute type there will be one or more DNs
738 # listed. For instance DCs normally have 3 hasMasterNCs
740 for value in res[0][k]:
741 # Turn dn into a dsdb_Dn so we can use
742 # its methods to parse a binary DN
743 dsdn = dsdb_Dn(samdb, value.decode('utf8'))
744 flags = dsdn.get_binary_integer()
747 if dnstr not in tmp_table:
748 rep = NCReplica(self, dnstr)
749 tmp_table[dnstr] = rep
751 rep = tmp_table[dnstr]
753 if k == "msDS-HasInstantiatedNCs":
754 rep.set_instantiated_flags(flags)
757 rep.identify_by_dsa_attr(samdb, k)
759 # if we've identified the default domain NC
760 # then save its DN string
762 self.default_dnstr = dnstr
764 raise KCCError("No nTDSDSA NCs for (%s)" % self.dsa_dnstr)
766 # Assign our newly built NC replica table to this dsa
767 self.current_rep_table = tmp_table
769 def add_needed_replica(self, rep):
770 """Method to add a NC replica that "should be present" to the
773 self.needed_rep_table[rep.nc_dnstr] = rep
775 def load_connection_table(self, samdb):
776 """Method to load the nTDSConnections listed for DSA object.
778 :param samdb: database to query for DSA connection list
781 res = samdb.search(base=self.dsa_dnstr,
782 scope=ldb.SCOPE_SUBTREE,
783 expression="(objectClass=nTDSConnection)")
785 except ldb.LdbError as e7:
786 (enum, estr) = e7.args
787 raise KCCError("Unable to find nTDSConnection for (%s) - (%s)" %
788 (self.dsa_dnstr, estr))
794 if dnstr in self.connect_table:
797 connect = NTDSConnection(dnstr)
799 connect.load_connection(samdb)
800 self.connect_table[dnstr] = connect
802 def commit_connections(self, samdb, ro=False):
803 """Method to commit any uncommitted nTDSConnections
804 modifications that are in our table. These would be
805 identified connections that are marked to be added or
808 :param samdb: database to commit DSA connection list to
809 :param ro: if (true) then peform internal operations but
810 do not write to the database (readonly)
814 for dnstr, connect in self.connect_table.items():
815 if connect.to_be_added:
816 connect.commit_added(samdb, ro)
818 if connect.to_be_modified:
819 connect.commit_modified(samdb, ro)
821 if connect.to_be_deleted:
822 connect.commit_deleted(samdb, ro)
823 delconn.append(dnstr)
825 # Now delete the connection from the table
826 for dnstr in delconn:
827 del self.connect_table[dnstr]
829 def add_connection(self, dnstr, connect):
830 assert dnstr not in self.connect_table
831 self.connect_table[dnstr] = connect
833 def get_connection_by_from_dnstr(self, from_dnstr):
834 """Scan DSA nTDSConnection table and return connection
835 with a "fromServer" dn string equivalent to method
838 :param from_dnstr: search for this from server entry
841 for connect in self.connect_table.values():
842 if connect.get_from_dnstr() == from_dnstr:
843 answer.append(connect)
847 def dumpstr_current_replica_table(self):
848 '''Debug dump string output of current replica table'''
849 return '\n'.join(str(x) for x in self.current_rep_table)
851 def dumpstr_needed_replica_table(self):
852 '''Debug dump string output of needed replica table'''
853 return '\n'.join(str(x) for x in self.needed_rep_table)
855 def dumpstr_connect_table(self):
856 '''Debug dump string output of connect table'''
857 return '\n'.join(str(x) for x in self.connect_table)
859 def new_connection(self, options, system_flags, transport, from_dnstr,
861 """Set up a new connection for the DSA based on input
862 parameters. Connection will be added to the DSA
863 connect_table and will be marked as "to be added" pending
864 a call to commit_connections()
866 dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
868 connect = NTDSConnection(dnstr)
869 connect.to_be_added = True
870 connect.enabled = True
871 connect.from_dnstr = from_dnstr
872 connect.options = options
873 connect.system_flags = system_flags
875 if transport is not None:
876 connect.transport_dnstr = transport.dnstr
877 connect.transport_guid = transport.guid
879 if sched is not None:
880 connect.schedule = sched
882 # Create schedule. Attribute valuse set according to MS-TECH
883 # intrasite connection creation document
884 connect.schedule = new_connection_schedule()
886 self.add_connection(dnstr, connect)
890 class NTDSConnection(object):
891 """Class defines a nTDSConnection found under a DSA
893 def __init__(self, dnstr):
898 self.to_be_added = False # new connection needs to be added
899 self.to_be_deleted = False # old connection needs to be deleted
900 self.to_be_modified = False
902 self.system_flags = 0
903 self.transport_dnstr = None
904 self.transport_guid = None
905 self.from_dnstr = None
909 '''Debug dump string output of NTDSConnection object'''
911 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
912 text = text + "\n\tenabled=%s" % self.enabled
913 text = text + "\n\tto_be_added=%s" % self.to_be_added
914 text = text + "\n\tto_be_deleted=%s" % self.to_be_deleted
915 text = text + "\n\tto_be_modified=%s" % self.to_be_modified
916 text = text + "\n\toptions=0x%08X" % self.options
917 text = text + "\n\tsystem_flags=0x%08X" % self.system_flags
918 text = text + "\n\twhenCreated=%d" % self.whenCreated
919 text = text + "\n\ttransport_dn=%s" % self.transport_dnstr
921 if self.guid is not None:
922 text = text + "\n\tguid=%s" % str(self.guid)
924 if self.transport_guid is not None:
925 text = text + "\n\ttransport_guid=%s" % str(self.transport_guid)
927 text = text + "\n\tfrom_dn=%s" % self.from_dnstr
929 if self.schedule is not None:
930 text += "\n\tschedule.size=%s" % self.schedule.size
931 text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
932 text += ("\n\tschedule.numberOfSchedules=%s" %
933 self.schedule.numberOfSchedules)
935 for i, header in enumerate(self.schedule.headerArray):
936 text += ("\n\tschedule.headerArray[%d].type=%d" %
938 text += ("\n\tschedule.headerArray[%d].offset=%d" %
940 text += "\n\tschedule.dataArray[%d].slots[ " % i
941 for slot in self.schedule.dataArray[i].slots:
942 text = text + "0x%X " % slot
947 def load_connection(self, samdb):
948 """Given a NTDSConnection object with an prior initialization
949 for the object's DN, search for the DN and load attributes
961 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
964 except ldb.LdbError as e8:
965 (enum, estr) = e8.args
966 raise KCCError("Unable to find nTDSConnection for (%s) - (%s)" %
972 self.options = int(msg["options"][0])
974 if "enabledConnection" in msg:
975 if str(msg["enabledConnection"][0]).upper().lstrip().rstrip() == "TRUE":
978 if "systemFlags" in msg:
979 self.system_flags = int(msg["systemFlags"][0])
983 misc.GUID(samdb.schema_format_value("objectGUID",
984 msg["objectGUID"][0]))
986 raise KCCError("Unable to find objectGUID in nTDSConnection "
987 "for (%s)" % (self.dnstr))
989 if "transportType" in msg:
990 dsdn = dsdb_Dn(samdb, msg["transportType"][0].decode('utf8'))
991 self.load_connection_transport(samdb, str(dsdn.dn))
993 if "schedule" in msg:
994 self.schedule = ndr_unpack(drsblobs.schedule, msg["schedule"][0])
996 if "whenCreated" in msg:
997 self.whenCreated = ldb.string_to_time(str(msg["whenCreated"][0]))
999 if "fromServer" in msg:
1000 dsdn = dsdb_Dn(samdb, msg["fromServer"][0].decode('utf8'))
1001 self.from_dnstr = str(dsdn.dn)
1002 assert self.from_dnstr is not None
1004 def load_connection_transport(self, samdb, tdnstr):
1005 """Given a NTDSConnection object which enumerates a transport
1006 DN, load the transport information for the connection object
1008 :param tdnstr: transport DN to load
1010 attrs = ["objectGUID"]
1012 res = samdb.search(base=tdnstr,
1013 scope=ldb.SCOPE_BASE, attrs=attrs)
1015 except ldb.LdbError as e9:
1016 (enum, estr) = e9.args
1017 raise KCCError("Unable to find transport (%s) - (%s)" %
1020 if "objectGUID" in res[0]:
1022 self.transport_dnstr = tdnstr
1023 self.transport_guid = \
1024 misc.GUID(samdb.schema_format_value("objectGUID",
1025 msg["objectGUID"][0]))
1026 assert self.transport_dnstr is not None
1027 assert self.transport_guid is not None
1029 def commit_deleted(self, samdb, ro=False):
1030 """Local helper routine for commit_connections() which
1031 handles committed connections that are to be deleted from
1032 the database database
1034 assert self.to_be_deleted
1035 self.to_be_deleted = False
1037 # No database modification requested
1042 samdb.delete(self.dnstr)
1043 except ldb.LdbError as e10:
1044 (enum, estr) = e10.args
1045 raise KCCError("Could not delete nTDSConnection for (%s) - (%s)" %
1048 def commit_added(self, samdb, ro=False):
1049 """Local helper routine for commit_connections() which
1050 handles committed connections that are to be added to the
1053 assert self.to_be_added
1054 self.to_be_added = False
1056 # No database modification requested
1060 # First verify we don't have this entry to ensure nothing
1061 # is programatically amiss
1064 msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1068 except ldb.LdbError as e11:
1069 (enum, estr) = e11.args
1070 if enum != ldb.ERR_NO_SUCH_OBJECT:
1071 raise KCCError("Unable to search for (%s) - (%s)" %
1074 raise KCCError("nTDSConnection for (%s) already exists!" %
1082 # Prepare a message for adding to the samdb
1084 m.dn = ldb.Dn(samdb, self.dnstr)
1086 m["objectClass"] = \
1087 ldb.MessageElement("nTDSConnection", ldb.FLAG_MOD_ADD,
1089 m["showInAdvancedViewOnly"] = \
1090 ldb.MessageElement("TRUE", ldb.FLAG_MOD_ADD,
1091 "showInAdvancedViewOnly")
1092 m["enabledConnection"] = \
1093 ldb.MessageElement(enablestr, ldb.FLAG_MOD_ADD,
1094 "enabledConnection")
1096 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_ADD, "fromServer")
1098 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options")
1099 m["systemFlags"] = \
1100 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_ADD,
1103 if self.transport_dnstr is not None:
1104 m["transportType"] = \
1105 ldb.MessageElement(str(self.transport_dnstr), ldb.FLAG_MOD_ADD,
1108 if self.schedule is not None:
1110 ldb.MessageElement(ndr_pack(self.schedule),
1111 ldb.FLAG_MOD_ADD, "schedule")
1114 except ldb.LdbError as e12:
1115 (enum, estr) = e12.args
1116 raise KCCError("Could not add nTDSConnection for (%s) - (%s)" %
1119 def commit_modified(self, samdb, ro=False):
1120 """Local helper routine for commit_connections() which
1121 handles committed connections that are to be modified to the
1124 assert self.to_be_modified
1125 self.to_be_modified = False
1127 # No database modification requested
1131 # First verify we have this entry to ensure nothing
1132 # is programatically amiss
1134 # we don't use the search result, but it tests the status
1135 # of self.dnstr in the database.
1136 samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1138 except ldb.LdbError as e13:
1139 (enum, estr) = e13.args
1140 if enum == ldb.ERR_NO_SUCH_OBJECT:
1141 raise KCCError("nTDSConnection for (%s) doesn't exist!" %
1143 raise KCCError("Unable to search for (%s) - (%s)" %
1151 # Prepare a message for modifying the samdb
1153 m.dn = ldb.Dn(samdb, self.dnstr)
1155 m["enabledConnection"] = \
1156 ldb.MessageElement(enablestr, ldb.FLAG_MOD_REPLACE,
1157 "enabledConnection")
1159 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_REPLACE,
1162 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_REPLACE,
1164 m["systemFlags"] = \
1165 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_REPLACE,
1168 if self.transport_dnstr is not None:
1169 m["transportType"] = \
1170 ldb.MessageElement(str(self.transport_dnstr),
1171 ldb.FLAG_MOD_REPLACE, "transportType")
1173 m["transportType"] = \
1174 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "transportType")
1176 if self.schedule is not None:
1178 ldb.MessageElement(ndr_pack(self.schedule),
1179 ldb.FLAG_MOD_REPLACE, "schedule")
1182 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "schedule")
1185 except ldb.LdbError as e14:
1186 (enum, estr) = e14.args
1187 raise KCCError("Could not modify nTDSConnection for (%s) - (%s)" %
1190 def set_modified(self, truefalse):
1191 self.to_be_modified = truefalse
1193 def is_schedule_minimum_once_per_week(self):
1194 """Returns True if our schedule includes at least one
1195 replication interval within the week. False otherwise
1197 # replinfo schedule is None means "always", while
1198 # NTDSConnection schedule is None means "never".
1199 if self.schedule is None or self.schedule.dataArray[0] is None:
1202 for slot in self.schedule.dataArray[0].slots:
1203 if (slot & 0x0F) != 0x0:
1207 def is_equivalent_schedule(self, sched):
1208 """Returns True if our schedule is equivalent to the input
1209 comparison schedule.
1211 :param shed: schedule to compare to
1213 # There are 4 cases, where either self.schedule or sched can be None
1215 # | self. is None | self. is not None
1216 # --------------+-----------------+--------------------
1217 # sched is None | True | False
1218 # --------------+-----------------+--------------------
1219 # sched is not None | False | do calculations
1221 if self.schedule is None:
1222 return sched is None
1227 if ((self.schedule.size != sched.size or
1228 self.schedule.bandwidth != sched.bandwidth or
1229 self.schedule.numberOfSchedules != sched.numberOfSchedules)):
1232 for i, header in enumerate(self.schedule.headerArray):
1234 if self.schedule.headerArray[i].type != sched.headerArray[i].type:
1237 if self.schedule.headerArray[i].offset != \
1238 sched.headerArray[i].offset:
1241 for a, b in zip(self.schedule.dataArray[i].slots,
1242 sched.dataArray[i].slots):
1247 def is_rodc_topology(self):
1248 """Returns True if NTDS Connection specifies RODC
1251 if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
1255 def is_generated(self):
1256 """Returns True if NTDS Connection was generated by the
1257 KCC topology algorithm as opposed to set by the administrator
1259 if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0:
1263 def is_override_notify_default(self):
1264 """Returns True if NTDS Connection should override notify default
1266 if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0:
1270 def is_use_notify(self):
1271 """Returns True if NTDS Connection should use notify
1273 if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0:
1277 def is_twoway_sync(self):
1278 """Returns True if NTDS Connection should use twoway sync
1280 if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0:
1284 def is_intersite_compression_disabled(self):
1285 """Returns True if NTDS Connection intersite compression
1288 if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0:
1292 def is_user_owned_schedule(self):
1293 """Returns True if NTDS Connection has a user owned schedule
1295 if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0:
1299 def is_enabled(self):
1300 """Returns True if NTDS Connection is enabled
1304 def get_from_dnstr(self):
1305 '''Return fromServer dn string attribute'''
1306 return self.from_dnstr
1309 class Partition(NamingContext):
1310 """A naming context discovered thru Partitions DN of the config schema.
1312 This is a more specific form of NamingContext class (inheriting from that
1313 class) and it identifies unique attributes enumerated in the Partitions
1314 such as which nTDSDSAs are cross referenced for replicas
1316 def __init__(self, partstr):
1317 self.partstr = partstr
1319 self.system_flags = 0
1320 self.rw_location_list = []
1321 self.ro_location_list = []
1323 # We don't have enough info to properly
1324 # fill in the naming context yet. We'll get that
1325 # fully set up with load_partition().
1326 NamingContext.__init__(self, None)
1328 def load_partition(self, samdb):
1329 """Given a Partition class object that has been initialized with its
1330 partition dn string, load the partition from the sam database, identify
1331 the type of the partition (schema, domain, etc) and record the list of
1332 nTDSDSAs that appear in the cross reference attributes
1333 msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations.
1335 :param samdb: sam database to load partition from
1340 "msDS-NC-Replica-Locations",
1341 "msDS-NC-RO-Replica-Locations"]
1343 res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE,
1346 except ldb.LdbError as e15:
1347 (enum, estr) = e15.args
1348 raise KCCError("Unable to find partition for (%s) - (%s)" %
1349 (self.partstr, estr))
1351 for k in msg.keys():
1356 if str(msg[k][0]).upper().lstrip().rstrip() == "TRUE":
1359 self.enabled = False
1362 if k == "systemFlags":
1363 self.system_flags = int(msg[k][0])
1366 for value in msg[k]:
1367 dsdn = dsdb_Dn(samdb, value.decode('utf8'))
1368 dnstr = str(dsdn.dn)
1371 self.nc_dnstr = dnstr
1374 if k == "msDS-NC-Replica-Locations":
1375 self.rw_location_list.append(dnstr)
1378 if k == "msDS-NC-RO-Replica-Locations":
1379 self.ro_location_list.append(dnstr)
1382 # Now identify what type of NC this partition
1384 self.identify_by_basedn(samdb)
1386 def is_enabled(self):
1387 """Returns True if partition is enabled
1389 return self.is_enabled
1391 def is_foreign(self):
1392 """Returns True if this is not an Active Directory NC in our
1393 forest but is instead something else (e.g. a foreign NC)
1395 if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0:
1400 def should_be_present(self, target_dsa):
1401 """Tests whether this partition should have an NC replica
1402 on the target dsa. This method returns a tuple of
1403 needed=True/False, ro=True/False, partial=True/False
1405 :param target_dsa: should NC be present on target dsa
1410 # If this is the config, schema, or default
1411 # domain NC for the target dsa then it should
1413 needed = (self.nc_type == NCType.config or
1414 self.nc_type == NCType.schema or
1415 (self.nc_type == NCType.domain and
1416 self.nc_dnstr == target_dsa.default_dnstr))
1418 # A writable replica of an application NC should be present
1419 # if there a cross reference to the target DSA exists. Depending
1420 # on whether the DSA is ro we examine which type of cross reference
1421 # to look for (msDS-NC-Replica-Locations or
1422 # msDS-NC-RO-Replica-Locations
1423 if self.nc_type == NCType.application:
1424 if target_dsa.is_ro():
1425 if target_dsa.dsa_dnstr in self.ro_location_list:
1428 if target_dsa.dsa_dnstr in self.rw_location_list:
1431 # If the target dsa is a gc then a partial replica of a
1432 # domain NC (other than the DSAs default domain) should exist
1433 # if there is also a cross reference for the DSA
1434 if (target_dsa.is_gc() and
1435 self.nc_type == NCType.domain and
1436 self.nc_dnstr != target_dsa.default_dnstr and
1437 (target_dsa.dsa_dnstr in self.ro_location_list or
1438 target_dsa.dsa_dnstr in self.rw_location_list)):
1442 # partial NCs are always readonly
1443 if needed and (target_dsa.is_ro() or partial):
1446 return needed, ro, partial
1449 '''Debug dump string output of class'''
1450 text = "%s" % NamingContext.__str__(self)
1451 text = text + "\n\tpartdn=%s" % self.partstr
1452 for k in self.rw_location_list:
1453 text = text + "\n\tmsDS-NC-Replica-Locations=%s" % k
1454 for k in self.ro_location_list:
1455 text = text + "\n\tmsDS-NC-RO-Replica-Locations=%s" % k
1460 """An individual site object discovered thru the configuration
1461 naming context. Contains all DSAs that exist within the site
1463 def __init__(self, site_dnstr, nt_now):
1464 self.site_dnstr = site_dnstr
1465 self.site_guid = None
1466 self.site_options = 0
1467 self.site_topo_generator = None
1468 self.site_topo_failover = 0 # appears to be in minutes
1470 self.rw_dsa_table = {}
1471 self.nt_now = nt_now
1473 def load_site(self, samdb):
1474 """Loads the NTDS Site Settings options attribute for the site
1475 as well as querying and loading all DSAs that appear within
1478 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1480 "interSiteTopologyFailover",
1481 "interSiteTopologyGenerator"]
1483 res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE,
1485 self_res = samdb.search(base=self.site_dnstr, scope=ldb.SCOPE_BASE,
1486 attrs=['objectGUID'])
1487 except ldb.LdbError as e16:
1488 (enum, estr) = e16.args
1489 raise KCCError("Unable to find site settings for (%s) - (%s)" %
1493 if "options" in msg:
1494 self.site_options = int(msg["options"][0])
1496 if "interSiteTopologyGenerator" in msg:
1497 self.site_topo_generator = \
1498 str(msg["interSiteTopologyGenerator"][0])
1500 if "interSiteTopologyFailover" in msg:
1501 self.site_topo_failover = int(msg["interSiteTopologyFailover"][0])
1504 if "objectGUID" in msg:
1505 self.site_guid = misc.GUID(samdb.schema_format_value("objectGUID",
1506 msg["objectGUID"][0]))
1508 self.load_all_dsa(samdb)
1510 def load_all_dsa(self, samdb):
1511 """Discover all nTDSDSA thru the sites entry and
1512 instantiate and load the DSAs. Each dsa is inserted
1513 into the dsa_table by dn string.
1516 res = samdb.search(self.site_dnstr,
1517 scope=ldb.SCOPE_SUBTREE,
1518 expression="(objectClass=nTDSDSA)")
1519 except ldb.LdbError as e17:
1520 (enum, estr) = e17.args
1521 raise KCCError("Unable to find nTDSDSAs - (%s)" % estr)
1527 if dnstr in self.dsa_table:
1530 dsa = DirectoryServiceAgent(dnstr)
1534 # Assign this dsa to my dsa table
1535 # and index by dsa dn
1536 self.dsa_table[dnstr] = dsa
1538 self.rw_dsa_table[dnstr] = dsa
1540 def get_dsa(self, dnstr):
1541 """Return a previously loaded DSA object by consulting
1542 the sites dsa_table for the provided DSA dn string
1544 :return: None if DSA doesn't exist
1546 return self.dsa_table.get(dnstr)
1548 def select_istg(self, samdb, mydsa, ro):
1549 """Determine if my DC should be an intersite topology
1550 generator. If my DC is the istg and is both a writeable
1551 DC and the database is opened in write mode then we perform
1552 an originating update to set the interSiteTopologyGenerator
1553 attribute in the NTDS Site Settings object. An RODC always
1554 acts as an ISTG for itself.
1556 # The KCC on an RODC always acts as an ISTG for itself
1558 mydsa.dsa_is_istg = True
1559 self.site_topo_generator = mydsa.dsa_dnstr
1562 c_rep = get_dsa_config_rep(mydsa)
1564 # Load repsFrom and replUpToDateVector if not already loaded
1565 # so we can get the current state of the config replica and
1566 # whether we are getting updates from the istg
1567 c_rep.load_repsFrom(samdb)
1569 c_rep.load_replUpToDateVector(samdb)
1571 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1572 # First, the KCC on a writable DC determines whether it acts
1573 # as an ISTG for its site
1575 # Let s be the object such that s!lDAPDisplayName = nTDSDSA
1576 # and classSchema in s!objectClass.
1578 # Let D be the sequence of objects o in the site of the local
1579 # DC such that o!objectCategory = s. D is sorted in ascending
1580 # order by objectGUID.
1582 # Which is a fancy way of saying "sort all the nTDSDSA objects
1583 # in the site by guid in ascending order". Place sorted list
1586 self.rw_dsa_table.values(),
1587 key=lambda dsa: ndr_pack(dsa.dsa_guid))
1589 # double word number of 100 nanosecond intervals since 1600s
1591 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1592 # if o!interSiteTopologyFailover is 0 or has no value.
1594 # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1595 # so it appears we have to turn f into the same interval
1597 # interSiteTopologyFailover (if set) appears to be in minutes
1598 # so we'll need to convert to senconds and then 100 nanosecond
1600 # XXX [MS-ADTS] 6.2.2.3.1 says it is seconds, not minutes.
1602 # 10,000,000 is number of 100 nanosecond intervals in a second
1603 if self.site_topo_failover == 0:
1604 f = 2 * 60 * 60 * 10000000
1606 f = self.site_topo_failover * 60 * 10000000
1608 # Let o be the site settings object for the site of the local
1609 # DC, or NULL if no such o exists.
1610 d_dsa = self.dsa_table.get(self.site_topo_generator)
1612 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1613 # If o != NULL and o!interSiteTopologyGenerator is not the
1614 # nTDSDSA object for the local DC and
1615 # o!interSiteTopologyGenerator is an element dj of sequence D:
1617 if d_dsa is not None and d_dsa is not mydsa:
1618 # From MS-ADTS 6.2.2.3.1 ISTG Selection:
1619 # Let c be the cursor in the replUpToDateVector variable
1620 # associated with the NC replica of the config NC such
1621 # that c.uuidDsa = dj!invocationId. If no such c exists
1622 # (No evidence of replication from current ITSG):
1626 # Else if the current time < c.timeLastSyncSuccess - f
1627 # (Evidence of time sync problem on current ISTG):
1631 # Else (Evidence of replication from current ITSG):
1633 # Let t = c.timeLastSyncSuccess.
1635 # last_success appears to be a double word containing
1636 # number of 100 nanosecond intervals since the 1600s
1637 j_idx = D_sort.index(d_dsa)
1640 for cursor in c_rep.rep_replUpToDateVector_cursors:
1641 if d_dsa.dsa_ivid == cursor.source_dsa_invocation_id:
1649 # XXX doc says current time < c.timeLastSyncSuccess - f
1650 # which is true only if f is negative or clocks are wrong.
1651 # f is not negative in the default case (2 hours).
1652 elif self.nt_now - cursor.last_sync_success > f:
1657 t_time = cursor.last_sync_success
1659 # Otherwise (Nominate local DC as ISTG):
1660 # Let i be the integer such that di is the nTDSDSA
1661 # object for the local DC.
1662 # Let t = the current time.
1664 i_idx = D_sort.index(mydsa)
1665 t_time = self.nt_now
1667 # Compute a function that maintains the current ISTG if
1668 # it is alive, cycles through other candidates if not.
1670 # Let k be the integer (i + ((current time - t) /
1671 # o!interSiteTopologyFailover)) MOD |D|.
1673 # Note: We don't want to divide by zero here so they must
1674 # have meant "f" instead of "o!interSiteTopologyFailover"
1675 k_idx = (i_idx + ((self.nt_now - t_time) // f)) % len(D_sort)
1677 # The local writable DC acts as an ISTG for its site if and
1678 # only if dk is the nTDSDSA object for the local DC. If the
1679 # local DC does not act as an ISTG, the KCC skips the
1680 # remainder of this task.
1681 d_dsa = D_sort[k_idx]
1682 d_dsa.dsa_is_istg = True
1684 # Update if we are the ISTG, otherwise return
1685 if d_dsa is not mydsa:
1689 if self.site_topo_generator == mydsa.dsa_dnstr:
1692 self.site_topo_generator = mydsa.dsa_dnstr
1694 # If readonly database then do not perform a
1699 # Perform update to the samdb
1700 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1703 m.dn = ldb.Dn(samdb, ssdn)
1705 m["interSiteTopologyGenerator"] = \
1706 ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
1707 "interSiteTopologyGenerator")
1711 except ldb.LdbError as estr:
1713 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1717 def is_intrasite_topology_disabled(self):
1718 '''Returns True if intra-site topology is disabled for site'''
1719 return (self.site_options &
1720 dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0
1722 def is_intersite_topology_disabled(self):
1723 '''Returns True if inter-site topology is disabled for site'''
1724 return ((self.site_options &
1725 dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED)
1728 def is_random_bridgehead_disabled(self):
1729 '''Returns True if selection of random bridgehead is disabled'''
1730 return (self.site_options &
1731 dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0
1733 def is_detect_stale_disabled(self):
1734 '''Returns True if detect stale is disabled for site'''
1735 return (self.site_options &
1736 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0
1738 def is_cleanup_ntdsconn_disabled(self):
1739 '''Returns True if NTDS Connection cleanup is disabled for site'''
1740 return (self.site_options &
1741 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0
1743 def same_site(self, dsa):
1744 '''Return True if dsa is in this site'''
1745 if self.get_dsa(dsa.dsa_dnstr):
1749 def is_rodc_site(self):
1750 if len(self.dsa_table) > 0 and len(self.rw_dsa_table) == 0:
1755 '''Debug dump string output of class'''
1756 text = "%s:" % self.__class__.__name__
1757 text = text + "\n\tdn=%s" % self.site_dnstr
1758 text = text + "\n\toptions=0x%X" % self.site_options
1759 text = text + "\n\ttopo_generator=%s" % self.site_topo_generator
1760 text = text + "\n\ttopo_failover=%d" % self.site_topo_failover
1761 for key, dsa in self.dsa_table.items():
1762 text = text + "\n%s" % dsa
1766 class GraphNode(object):
1767 """A graph node describing a set of edges that should be directed to it.
1769 Each edge is a connection for a particular naming context replica directed
1770 from another node in the forest to this node.
1773 def __init__(self, dsa_dnstr, max_node_edges):
1774 """Instantiate the graph node according to a DSA dn string
1776 :param max_node_edges: maximum number of edges that should ever
1777 be directed to the node
1779 self.max_edges = max_node_edges
1780 self.dsa_dnstr = dsa_dnstr
1784 text = "%s:" % self.__class__.__name__
1785 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
1786 text = text + "\n\tmax_edges=%d" % self.max_edges
1788 for i, edge in enumerate(self.edge_from):
1789 if isinstance(edge, str):
1790 text += "\n\tedge_from[%d]=%s" % (i, edge)
1794 def add_edge_from(self, from_dsa_dnstr):
1795 """Add an edge from the dsa to our graph nodes edge from list
1797 :param from_dsa_dnstr: the dsa that the edge emanates from
1799 assert isinstance(from_dsa_dnstr, str)
1801 # No edges from myself to myself
1802 if from_dsa_dnstr == self.dsa_dnstr:
1804 # Only one edge from a particular node
1805 if from_dsa_dnstr in self.edge_from:
1807 # Not too many edges
1808 if len(self.edge_from) >= self.max_edges:
1810 self.edge_from.append(from_dsa_dnstr)
1813 def add_edges_from_connections(self, dsa):
1814 """For each nTDSConnection object associated with a particular
1815 DSA, we test if it implies an edge to this graph node (i.e.
1816 the "fromServer" attribute). If it does then we add an
1817 edge from the server unless we are over the max edges for this
1820 :param dsa: dsa with a dnstr equivalent to his graph node
1822 for connect in dsa.connect_table.values():
1823 self.add_edge_from(connect.from_dnstr)
1825 def add_connections_from_edges(self, dsa, transport):
1826 """For each edge directed to this graph node, ensure there
1827 is a corresponding nTDSConnection object in the dsa.
1829 for edge_dnstr in self.edge_from:
1830 connections = dsa.get_connection_by_from_dnstr(edge_dnstr)
1832 # For each edge directed to the NC replica that
1833 # "should be present" on the local DC, the KCC determines
1834 # whether an object c exists such that:
1836 # c is a child of the DC's nTDSDSA object.
1837 # c.objectCategory = nTDSConnection
1839 # Given the NC replica ri from which the edge is directed,
1840 # c.fromServer is the dsname of the nTDSDSA object of
1841 # the DC on which ri "is present".
1843 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1846 for connect in connections:
1847 if connect.is_rodc_topology():
1854 # if no such object exists then the KCC adds an object
1855 # c with the following attributes
1857 # Generate a new dnstr for this nTDSConnection
1858 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1859 flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1860 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1862 dsa.new_connection(opt, flags, transport, edge_dnstr, None)
1864 def has_sufficient_edges(self):
1865 '''Return True if we have met the maximum "from edges" criteria'''
1866 if len(self.edge_from) >= self.max_edges:
1871 class Transport(object):
1872 """Class defines a Inter-site transport found under Sites
1875 def __init__(self, dnstr):
1880 self.address_attr = None
1881 self.bridgehead_list = []
1884 '''Debug dump string output of Transport object'''
1886 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
1887 text = text + "\n\tguid=%s" % str(self.guid)
1888 text = text + "\n\toptions=%d" % self.options
1889 text = text + "\n\taddress_attr=%s" % self.address_attr
1890 text = text + "\n\tname=%s" % self.name
1891 for dnstr in self.bridgehead_list:
1892 text = text + "\n\tbridgehead_list=%s" % dnstr
1896 def load_transport(self, samdb):
1897 """Given a Transport object with an prior initialization
1898 for the object's DN, search for the DN and load attributes
1901 attrs = ["objectGUID",
1904 "bridgeheadServerListBL",
1905 "transportAddressAttribute"]
1907 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
1910 except ldb.LdbError as e18:
1911 (enum, estr) = e18.args
1912 raise KCCError("Unable to find Transport for (%s) - (%s)" %
1916 self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
1917 msg["objectGUID"][0]))
1919 if "options" in msg:
1920 self.options = int(msg["options"][0])
1922 if "transportAddressAttribute" in msg:
1923 self.address_attr = str(msg["transportAddressAttribute"][0])
1926 self.name = str(msg["name"][0])
1928 if "bridgeheadServerListBL" in msg:
1929 for value in msg["bridgeheadServerListBL"]:
1930 dsdn = dsdb_Dn(samdb, value.decode('utf8'))
1931 dnstr = str(dsdn.dn)
1932 if dnstr not in self.bridgehead_list:
1933 self.bridgehead_list.append(dnstr)
1936 class RepsFromTo(object):
1937 """Class encapsulation of the NDR repsFromToBlob.
1939 Removes the necessity of external code having to
1940 understand about other_info or manipulation of
1943 def __init__(self, nc_dnstr=None, ndr_blob=None):
1945 self.__dict__['to_be_deleted'] = False
1946 self.__dict__['nc_dnstr'] = nc_dnstr
1947 self.__dict__['update_flags'] = 0x0
1948 # XXX the following sounds dubious and/or better solved
1949 # elsewhere, but lets leave it for now. In particular, there
1950 # seems to be no reason for all the non-ndr generated
1951 # attributes to be handled in the round about way (e.g.
1952 # self.__dict__['to_be_deleted'] = False above). On the other
1953 # hand, it all seems to work. Hooray! Hands off!.
1957 # There is a very subtle bug here with python
1958 # and our NDR code. If you assign directly to
1959 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1960 # then a proper python GC reference count is not
1963 # To work around this we maintain an internal
1964 # reference to "dns_name(x)" and "other_info" elements
1965 # of repsFromToBlob. This internal reference
1966 # is hidden within this class but it is why you
1967 # see statements like this below:
1969 # self.__dict__['ndr_blob'].ctr.other_info = \
1970 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1972 # That would appear to be a redundant assignment but
1973 # it is necessary to hold a proper python GC reference
1975 if ndr_blob is None:
1976 self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
1977 self.__dict__['ndr_blob'].version = 0x1
1978 self.__dict__['dns_name1'] = None
1979 self.__dict__['dns_name2'] = None
1981 self.__dict__['ndr_blob'].ctr.other_info = \
1982 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1985 self.__dict__['ndr_blob'] = ndr_blob
1986 self.__dict__['other_info'] = ndr_blob.ctr.other_info
1988 if ndr_blob.version == 0x1:
1989 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
1990 self.__dict__['dns_name2'] = None
1992 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
1993 self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
1996 '''Debug dump string output of class'''
1998 text = "%s:" % self.__class__.__name__
1999 text += "\n\tdnstr=%s" % self.nc_dnstr
2000 text += "\n\tupdate_flags=0x%X" % self.update_flags
2001 text += "\n\tversion=%d" % self.version
2002 text += "\n\tsource_dsa_obj_guid=%s" % self.source_dsa_obj_guid
2003 text += ("\n\tsource_dsa_invocation_id=%s" %
2004 self.source_dsa_invocation_id)
2005 text += "\n\ttransport_guid=%s" % self.transport_guid
2006 text += "\n\treplica_flags=0x%X" % self.replica_flags
2007 text += ("\n\tconsecutive_sync_failures=%d" %
2008 self.consecutive_sync_failures)
2009 text += "\n\tlast_success=%s" % self.last_success
2010 text += "\n\tlast_attempt=%s" % self.last_attempt
2011 text += "\n\tdns_name1=%s" % self.dns_name1
2012 text += "\n\tdns_name2=%s" % self.dns_name2
2013 text += "\n\tschedule[ "
2014 for slot in self.schedule:
2015 text += "0x%X " % slot
2020 def __setattr__(self, item, value):
2021 """Set an attribute and chyange update flag.
2023 Be aware that setting any RepsFromTo attribute will set the
2024 drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS update flag.
2026 if item in ['schedule', 'replica_flags', 'transport_guid',
2027 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2028 'consecutive_sync_failures', 'last_success',
2031 if item in ['replica_flags']:
2032 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
2033 elif item in ['schedule']:
2034 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
2036 setattr(self.__dict__['ndr_blob'].ctr, item, value)
2038 elif item in ['dns_name1']:
2039 self.__dict__['dns_name1'] = value
2041 if self.__dict__['ndr_blob'].version == 0x1:
2042 self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
2043 self.__dict__['dns_name1']
2045 self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
2046 self.__dict__['dns_name1']
2048 elif item in ['dns_name2']:
2049 self.__dict__['dns_name2'] = value
2051 if self.__dict__['ndr_blob'].version == 0x1:
2052 raise AttributeError(item)
2054 self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
2055 self.__dict__['dns_name2']
2057 elif item in ['nc_dnstr']:
2058 self.__dict__['nc_dnstr'] = value
2060 elif item in ['to_be_deleted']:
2061 self.__dict__['to_be_deleted'] = value
2063 elif item in ['version']:
2064 raise AttributeError("Attempt to set readonly attribute %s" % item)
2066 raise AttributeError("Unknown attribute %s" % item)
2068 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
2070 def __getattr__(self, item):
2071 """Overload of RepsFromTo attribute retrieval.
2073 Allows external code to ignore substructures within the blob
2075 if item in ['schedule', 'replica_flags', 'transport_guid',
2076 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2077 'consecutive_sync_failures', 'last_success',
2079 return getattr(self.__dict__['ndr_blob'].ctr, item)
2081 elif item in ['version']:
2082 return self.__dict__['ndr_blob'].version
2084 elif item in ['dns_name1']:
2085 if self.__dict__['ndr_blob'].version == 0x1:
2086 return self.__dict__['ndr_blob'].ctr.other_info.dns_name
2088 return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
2090 elif item in ['dns_name2']:
2091 if self.__dict__['ndr_blob'].version == 0x1:
2092 raise AttributeError(item)
2094 return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
2096 elif item in ['to_be_deleted']:
2097 return self.__dict__['to_be_deleted']
2099 elif item in ['nc_dnstr']:
2100 return self.__dict__['nc_dnstr']
2102 elif item in ['update_flags']:
2103 return self.__dict__['update_flags']
2105 raise AttributeError("Unknown attribute %s" % item)
2107 def is_modified(self):
2108 return (self.update_flags != 0x0)
2110 def set_unmodified(self):
2111 self.__dict__['update_flags'] = 0x0
2114 class SiteLink(object):
2115 """Class defines a site link found under sites
2118 def __init__(self, dnstr):
2121 self.system_flags = 0
2123 self.schedule = None
2124 self.interval = None
2128 '''Debug dump string output of Transport object'''
2130 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
2131 text = text + "\n\toptions=%d" % self.options
2132 text = text + "\n\tsystem_flags=%d" % self.system_flags
2133 text = text + "\n\tcost=%d" % self.cost
2134 text = text + "\n\tinterval=%s" % self.interval
2136 if self.schedule is not None:
2137 text += "\n\tschedule.size=%s" % self.schedule.size
2138 text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
2139 text += ("\n\tschedule.numberOfSchedules=%s" %
2140 self.schedule.numberOfSchedules)
2142 for i, header in enumerate(self.schedule.headerArray):
2143 text += ("\n\tschedule.headerArray[%d].type=%d" %
2145 text += ("\n\tschedule.headerArray[%d].offset=%d" %
2147 text = text + "\n\tschedule.dataArray[%d].slots[ " % i
2148 for slot in self.schedule.dataArray[i].slots:
2149 text = text + "0x%X " % slot
2152 for guid, dn in self.site_list:
2153 text = text + "\n\tsite_list=%s (%s)" % (guid, dn)
2156 def load_sitelink(self, samdb):
2157 """Given a siteLink object with an prior initialization
2158 for the object's DN, search for the DN and load attributes
2168 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
2169 attrs=attrs, controls=['extended_dn:0'])
2171 except ldb.LdbError as e19:
2172 (enum, estr) = e19.args
2173 raise KCCError("Unable to find SiteLink for (%s) - (%s)" %
2178 if "options" in msg:
2179 self.options = int(msg["options"][0])
2181 if "systemFlags" in msg:
2182 self.system_flags = int(msg["systemFlags"][0])
2185 self.cost = int(msg["cost"][0])
2187 if "replInterval" in msg:
2188 self.interval = int(msg["replInterval"][0])
2190 if "siteList" in msg:
2191 for value in msg["siteList"]:
2192 dsdn = dsdb_Dn(samdb, value.decode('utf8'))
2193 guid = misc.GUID(dsdn.dn.get_extended_component('GUID'))
2194 dnstr = str(dsdn.dn)
2195 if (guid, dnstr) not in self.site_list:
2196 self.site_list.append((guid, dnstr))
2198 if "schedule" in msg:
2199 self.schedule = ndr_unpack(drsblobs.schedule, value)
2201 self.schedule = new_connection_schedule()
2204 class KCCFailedObject(object):
2205 def __init__(self, uuid, failure_count, time_first_failure,
2206 last_result, dns_name):
2208 self.failure_count = failure_count
2209 self.time_first_failure = time_first_failure
2210 self.last_result = last_result
2211 self.dns_name = dns_name
2214 ##################################################
2215 # Global Functions and Variables
2216 ##################################################
2218 def get_dsa_config_rep(dsa):
2219 # Find configuration NC replica for the DSA
2220 for c_rep in dsa.current_rep_table.values():
2221 if c_rep.is_config():
2224 raise KCCError("Unable to find config NC replica for (%s)" %
2228 def new_connection_schedule():
2229 """Create a default schedule for an NTDSConnection or Sitelink. This
2230 is packed differently from the repltimes schedule used elsewhere
2231 in KCC (where the 168 nibbles are packed into 84 bytes).
2233 # 168 byte instances of the 0x01 value. The low order 4 bits
2234 # of the byte equate to 15 minute intervals within a single hour.
2235 # There are 168 bytes because there are 168 hours in a full week
2236 # Effectively we are saying to perform replication at the end of
2237 # each hour of the week
2238 schedule = drsblobs.schedule()
2241 schedule.bandwidth = 0
2242 schedule.numberOfSchedules = 1
2244 header = drsblobs.scheduleHeader()
2248 schedule.headerArray = [header]
2250 data = drsblobs.scheduleSlots()
2251 data.slots = [0x01] * 168
2253 schedule.dataArray = [data]
2257 ##################################################
2259 ##################################################
2261 def uncovered_sites_to_cover(samdb, site_name):
2263 Discover which sites have no DCs and whose lowest single-hop cost
2264 distance for any link attached to that site is linked to the site supplied.
2266 We compare the lowest cost of your single-hop link to this site to all of
2267 those available (if it exists). This means that a lower ranked siteLink
2268 with only the uncovered site can trump any available links (but this can
2269 only be done with specific, poorly enacted user configuration).
2271 If the site is connected to more than one other site with the same
2272 siteLink, only the largest site (failing that sorted alphabetically)
2273 creates the DNS records.
2275 :param samdb database
2276 :param site_name origin site (with a DC)
2278 :return a list of sites this site should be covering (for DNS)
2282 server_res = samdb.search(base=samdb.get_config_basedn(),
2283 scope=ldb.SCOPE_SUBTREE,
2284 expression="(&(objectClass=server)"
2285 "(serverReference=*))")
2287 site_res = samdb.search(base=samdb.get_config_basedn(),
2288 scope=ldb.SCOPE_SUBTREE,
2289 expression="(objectClass=site)")
2291 sites_in_use = Counter()
2294 # Assume server is of form DC,Servers,Site-ABCD because of schema
2295 for msg in server_res:
2296 site_dn = msg.dn.parent().parent()
2297 sites_in_use[site_dn.canonical_str()] += 1
2299 if site_dn.get_rdn_value().lower() == site_name.lower():
2302 if len(sites_in_use) != len(site_res):
2303 # There is a possible uncovered site
2304 sites_uncovered = []
2306 for msg in site_res:
2307 if msg.dn.canonical_str() not in sites_in_use:
2308 sites_uncovered.append(msg)
2310 own_site_dn = "CN={},CN=Sites,{}".format(
2311 ldb.binary_encode(site_name),
2312 ldb.binary_encode(str(samdb.get_config_basedn()))
2315 for site in sites_uncovered:
2316 encoded_dn = ldb.binary_encode(str(site.dn))
2318 # Get a sorted list of all siteLinks featuring the uncovered site
2319 link_res1 = samdb.search(base=samdb.get_config_basedn(),
2320 scope=ldb.SCOPE_SUBTREE, attrs=["cost"],
2321 expression="(&(objectClass=siteLink)"
2322 "(siteList={}))".format(encoded_dn),
2323 controls=["server_sort:1:0:cost"])
2325 # Get a sorted list of all siteLinks connecting this an the
2327 link_res2 = samdb.search(base=samdb.get_config_basedn(),
2328 scope=ldb.SCOPE_SUBTREE,
2329 attrs=["cost", "siteList"],
2330 expression="(&(objectClass=siteLink)"
2331 "(siteList={})(siteList={}))".format(
2334 controls=["server_sort:1:0:cost"])
2336 # Add to list if your link is equal in cost to lowest cost link
2337 if len(link_res1) > 0 and len(link_res2) > 0:
2338 cost1 = int(link_res1[0]['cost'][0])
2339 cost2 = int(link_res2[0]['cost'][0])
2341 # Own siteLink must match the lowest cost link
2345 # In a siteLink with more than 2 sites attached, only pick the
2346 # largest site, and if there are multiple, the earliest
2349 for site_val in link_res2[0]['siteList']:
2350 site_dn = ldb.Dn(samdb, str(site_val))
2351 site_dn_str = site_dn.canonical_str()
2352 site_rdn = site_dn.get_rdn_value().lower()
2353 if sites_in_use[site_dn_str] > dc_count:
2356 elif (sites_in_use[site_dn_str] == dc_count and
2357 site_rdn < site_name.lower()):
2362 site_cover_rdn = site.dn.get_rdn_value()
2363 sites_to_cover.append(site_cover_rdn.lower())
2365 return sites_to_cover