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
36 class KCCError(Exception):
41 (unknown, schema, domain, config, application) = range(0, 5)
43 # map the NCType enum to strings for debugging
44 nctype_lut = dict((v, k) for k, v in NCType.__dict__.items() if k[:2] != '__')
47 class NamingContext(object):
48 """Base class for a naming context.
50 Holds the DN, GUID, SID (if available) and type of the DN.
51 Subclasses may inherit from this and specialize
54 def __init__(self, nc_dnstr):
55 """Instantiate a NamingContext
57 :param nc_dnstr: NC dn string
59 self.nc_dnstr = nc_dnstr
62 self.nc_type = NCType.unknown
65 '''Debug dump string output of class'''
66 text = "%s:" % (self.__class__.__name__,)
67 text = text + "\n\tnc_dnstr=%s" % self.nc_dnstr
68 text = text + "\n\tnc_guid=%s" % str(self.nc_guid)
70 if self.nc_sid is None:
71 text = text + "\n\tnc_sid=<absent>"
73 text = text + "\n\tnc_sid=<present>"
75 text = text + "\n\tnc_type=%s (%s)" % (nctype_lut[self.nc_type],
79 def load_nc(self, samdb):
80 attrs = ["objectGUID",
83 res = samdb.search(base=self.nc_dnstr,
84 scope=ldb.SCOPE_BASE, attrs=attrs)
86 except ldb.LdbError, (enum, estr):
87 raise KCCError("Unable to find naming context (%s) - (%s)" %
88 (self.nc_dnstr, estr))
90 if "objectGUID" in msg:
91 self.nc_guid = misc.GUID(samdb.schema_format_value("objectGUID",
92 msg["objectGUID"][0]))
93 if "objectSid" in msg:
94 self.nc_sid = msg["objectSid"][0]
96 assert self.nc_guid is not None
99 '''Return True if NC is config'''
100 assert self.nc_type != NCType.unknown
101 return self.nc_type == NCType.config
103 def identify_by_basedn(self, samdb):
104 """Given an NC object, identify what type is is thru
105 the samdb basedn strings and NC sid value
107 # Invoke loader to initialize guid and more
108 # importantly sid value (sid is used to identify
110 if self.nc_guid is None:
113 # We check against schema and config because they
114 # will be the same for all nTDSDSAs in the forest.
115 # That leaves the domain NCs which can be identified
116 # by sid and application NCs as the last identified
117 if self.nc_dnstr == str(samdb.get_schema_basedn()):
118 self.nc_type = NCType.schema
119 elif self.nc_dnstr == str(samdb.get_config_basedn()):
120 self.nc_type = NCType.config
121 elif self.nc_sid is not None:
122 self.nc_type = NCType.domain
124 self.nc_type = NCType.application
126 def identify_by_dsa_attr(self, samdb, attr):
127 """Given an NC which has been discovered thru the
128 nTDSDSA database object, determine what type of NC
129 it is (i.e. schema, config, domain, application) via
130 the use of the schema attribute under which the NC
133 :param attr: attr of nTDSDSA object where NC DN appears
135 # If the NC is listed under msDS-HasDomainNCs then
136 # this can only be a domain NC and it is our default
137 # domain for this dsa
138 if attr == "msDS-HasDomainNCs":
139 self.nc_type = NCType.domain
141 # If the NC is listed under hasPartialReplicaNCs
142 # this is only a domain NC
143 elif attr == "hasPartialReplicaNCs":
144 self.nc_type = NCType.domain
146 # NCs listed under hasMasterNCs are either
147 # default domain, schema, or config. We
148 # utilize the identify_by_basedn() to
150 elif attr == "hasMasterNCs":
151 self.identify_by_basedn(samdb)
153 # Still unknown (unlikely) but for completeness
154 # and for finally identifying application NCs
155 if self.nc_type == NCType.unknown:
156 self.identify_by_basedn(samdb)
159 class NCReplica(NamingContext):
160 """Naming context replica that is relative to a specific DSA.
162 This is a more specific form of NamingContext class (inheriting from that
163 class) and it identifies unique attributes of the DSA's replica for a NC.
166 def __init__(self, dsa_dnstr, dsa_guid, nc_dnstr):
167 """Instantiate a Naming Context Replica
169 :param dsa_guid: GUID of DSA where replica appears
170 :param nc_dnstr: NC dn string
172 self.rep_dsa_dnstr = dsa_dnstr
173 self.rep_dsa_guid = dsa_guid
174 self.rep_default = False # replica for DSA's default domain
175 self.rep_partial = False
177 self.rep_instantiated_flags = 0
179 self.rep_fsmo_role_owner = None
182 self.rep_repsFrom = []
187 # The (is present) test is a combination of being
188 # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or
189 # hasPartialReplicaNCs) as well as its replica flags found
190 # thru the msDS-HasInstantiatedNCs. If the NC replica meets
191 # the first enumeration test then this flag is set true
192 self.rep_present_criteria_one = False
194 # Call my super class we inherited from
195 NamingContext.__init__(self, nc_dnstr)
198 '''Debug dump string output of class'''
199 text = "%s:" % self.__class__.__name__
200 text = text + "\n\tdsa_dnstr=%s" % self.rep_dsa_dnstr
201 text = text + "\n\tdsa_guid=%s" % self.rep_dsa_guid
202 text = text + "\n\tdefault=%s" % self.rep_default
203 text = text + "\n\tro=%s" % self.rep_ro
204 text = text + "\n\tpartial=%s" % self.rep_partial
205 text = text + "\n\tpresent=%s" % self.is_present()
206 text = text + "\n\tfsmo_role_owner=%s" % self.rep_fsmo_role_owner
208 for rep in self.rep_repsFrom:
209 text = text + "\n%s" % rep
211 for rep in self.rep_repsTo:
212 text = text + "\n%s" % rep
214 return "%s\n%s" % (NamingContext.__str__(self), text)
216 def set_instantiated_flags(self, flags=None):
217 '''Set or clear NC replica instantiated flags'''
219 self.rep_instantiated_flags = 0
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, (enum, estr):
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, 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, (enum, estr):
396 raise KCCError("Unable to find NC for (%s) - (%s)" %
397 (self.nc_dnstr, estr))
401 # Possibly no replUpToDateVector if this is a singleton DC
402 if "replUpToDateVector" in msg:
403 value = msg["replUpToDateVector"][0]
404 blob = ndr_unpack(drsblobs.replUpToDateVectorBlob,
406 if blob.version != 2:
407 # Samba only generates version 2, and this runs locally
408 raise AttributeError("Unexpected replUpToDateVector version %d"
411 self.rep_replUpToDateVector_cursors = blob.ctr.cursors
413 self.rep_replUpToDateVector_cursors = []
415 def dumpstr_to_be_deleted(self):
416 return '\n'.join(str(x) for x in self.rep_repsFrom if x.to_be_deleted)
418 def dumpstr_to_be_modified(self):
419 return '\n'.join(str(x) for x in self.rep_repsFrom if x.is_modified())
421 def load_fsmo_roles(self, samdb):
422 """Given an NC replica which has been discovered thru the nTDSDSA
423 database object, load the fSMORoleOwner attribute.
426 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
427 attrs=["fSMORoleOwner"])
429 except ldb.LdbError, (enum, estr):
430 raise KCCError("Unable to find NC for (%s) - (%s)" %
431 (self.nc_dnstr, estr))
435 # Possibly no fSMORoleOwner
436 if "fSMORoleOwner" in msg:
437 self.rep_fsmo_role_owner = msg["fSMORoleOwner"]
439 def is_fsmo_role_owner(self, dsa_dnstr):
440 if self.rep_fsmo_role_owner is not None and \
441 self.rep_fsmo_role_owner == dsa_dnstr:
445 def load_repsTo(self, samdb):
446 """Given an NC replica which has been discovered thru the nTDSDSA
447 database object, load the repsTo attribute for the local replica.
448 held by my dsa. The repsTo attribute is not replicated so this
449 attribute is relative only to the local DSA that the samdb exists on
451 This is responsible for push replication, not scheduled pull
452 replication. Not to be confused for repsFrom.
455 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
458 except ldb.LdbError, (enum, estr):
459 raise KCCError("Unable to find NC for (%s) - (%s)" %
460 (self.nc_dnstr, estr))
464 # Possibly no repsTo if this is a singleton DC
466 for value in msg["repsTo"]:
467 rep = RepsFromTo(self.nc_dnstr,
468 ndr_unpack(drsblobs.repsFromToBlob, value))
469 self.rep_repsTo.append(rep)
471 def commit_repsTo(self, samdb, ro=False):
472 """Commit repsTo to the database"""
474 # XXX - This is not truly correct according to the MS-TECH
475 # docs. To commit a repsTo we should be using RPCs
476 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
477 # IDL_DRSReplicaDel to affect a repsTo change.
479 # Those RPCs are missing in samba, so I'll have to
480 # implement them to get this to more accurately
481 # reflect the reference docs. As of right now this
482 # commit to the database will work as its what the
488 for repsTo in self.rep_repsTo:
490 # Leave out any to be deleted from
491 # replacement list. Build a list
492 # of to be deleted reps which we will
493 # remove from rep_repsTo list below
494 if repsTo.to_be_deleted:
495 delreps.append(repsTo)
499 if repsTo.is_modified():
500 repsTo.set_unmodified()
503 # current (unmodified) elements also get
504 # appended here but no changes will occur
505 # unless something is "to be modified" or
507 newreps.append(ndr_pack(repsTo.ndr_blob))
509 # Now delete these from our list of rep_repsTo
510 for repsTo in delreps:
511 self.rep_repsTo.remove(repsTo)
514 # Nothing to do if no reps have been modified or
515 # need to be deleted or input option has informed
516 # us to be "readonly" (ro). Leave database
522 m.dn = ldb.Dn(samdb, self.nc_dnstr)
525 ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsTo")
530 except ldb.LdbError, estr:
531 raise KCCError("Could not set repsTo for (%s) - (%s)" %
532 (self.nc_dnstr, estr))
535 class DirectoryServiceAgent(object):
537 def __init__(self, dsa_dnstr):
538 """Initialize DSA class.
540 Class is subsequently fully populated by calling the load_dsa() method
542 :param dsa_dnstr: DN of the nTDSDSA
544 self.dsa_dnstr = dsa_dnstr
547 self.dsa_is_ro = False
548 self.dsa_is_istg = False
550 self.dsa_behavior = 0
551 self.default_dnstr = None # default domain dn string for dsa
553 # NCReplicas for this dsa that are "present"
554 # Indexed by DN string of naming context
555 self.current_rep_table = {}
557 # NCReplicas for this dsa that "should be present"
558 # Indexed by DN string of naming context
559 self.needed_rep_table = {}
561 # NTDSConnections for this dsa. These are current
562 # valid connections that are committed or pending a commit
563 # in the database. Indexed by DN string of connection
564 self.connect_table = {}
567 '''Debug dump string output of class'''
569 text = "%s:" % self.__class__.__name__
570 if self.dsa_dnstr is not None:
571 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
572 if self.dsa_guid is not None:
573 text = text + "\n\tdsa_guid=%s" % str(self.dsa_guid)
574 if self.dsa_ivid is not None:
575 text = text + "\n\tdsa_ivid=%s" % str(self.dsa_ivid)
577 text = text + "\n\tro=%s" % self.is_ro()
578 text = text + "\n\tgc=%s" % self.is_gc()
579 text = text + "\n\tistg=%s" % self.is_istg()
581 text = text + "\ncurrent_replica_table:"
582 text = text + "\n%s" % self.dumpstr_current_replica_table()
583 text = text + "\nneeded_replica_table:"
584 text = text + "\n%s" % self.dumpstr_needed_replica_table()
585 text = text + "\nconnect_table:"
586 text = text + "\n%s" % self.dumpstr_connect_table()
590 def get_current_replica(self, nc_dnstr):
591 return self.current_rep_table.get(nc_dnstr)
594 '''Returns True if dsa is intersite topology generator for it's site'''
595 # The KCC on an RODC always acts as an ISTG for itself
596 return self.dsa_is_istg or self.dsa_is_ro
599 '''Returns True if dsa a read only domain controller'''
600 return self.dsa_is_ro
603 '''Returns True if dsa hosts a global catalog'''
604 if (self.options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
608 def is_minimum_behavior(self, version):
609 """Is dsa at minimum windows level greater than or equal to (version)
611 :param version: Windows version to test against
612 (e.g. DS_DOMAIN_FUNCTION_2008)
614 if self.dsa_behavior >= version:
618 def is_translate_ntdsconn_disabled(self):
619 """Whether this allows NTDSConnection translation in its options."""
620 if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0:
624 def get_rep_tables(self):
625 """Return DSA current and needed replica tables
627 return self.current_rep_table, self.needed_rep_table
629 def get_parent_dnstr(self):
630 """Get the parent DN string of this object."""
631 head, sep, tail = self.dsa_dnstr.partition(',')
634 def load_dsa(self, samdb):
635 """Load a DSA from the samdb.
637 Prior initialization has given us the DN of the DSA that we are to
638 load. This method initializes all other attributes, including loading
639 the NC replica table for this DSA.
641 attrs = ["objectGUID",
645 "msDS-Behavior-Version"]
647 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
650 except ldb.LdbError, (enum, estr):
651 raise KCCError("Unable to find nTDSDSA for (%s) - (%s)" %
652 (self.dsa_dnstr, estr))
655 self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID",
656 msg["objectGUID"][0]))
658 # RODCs don't originate changes and thus have no invocationId,
659 # therefore we must check for existence first
660 if "invocationId" in msg:
661 self.dsa_ivid = misc.GUID(samdb.schema_format_value("objectGUID",
662 msg["invocationId"][0]))
665 self.options = int(msg["options"][0])
667 if "msDS-isRODC" in msg and msg["msDS-isRODC"][0] == "TRUE":
668 self.dsa_is_ro = True
670 self.dsa_is_ro = False
672 if "msDS-Behavior-Version" in msg:
673 self.dsa_behavior = int(msg['msDS-Behavior-Version'][0])
675 # Load the NC replicas that are enumerated on this dsa
676 self.load_current_replica_table(samdb)
678 # Load the nTDSConnection that are enumerated on this dsa
679 self.load_connection_table(samdb)
681 def load_current_replica_table(self, samdb):
682 """Method to load the NC replica's listed for DSA object.
684 This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs,
685 hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, and
686 msDS-HasInstantiatedNCs) to determine complete list of NC replicas that
687 are enumerated for the DSA. Once a NC replica is loaded it is
688 identified (schema, config, etc) and the other replica attributes
689 (partial, ro, etc) are determined.
691 :param samdb: database to query for DSA replica list
694 # not RODC - default, config, schema (old style)
696 # not RODC - default, config, schema, app NCs
698 # domain NC partial replicas
699 "hasPartialReplicaNCs",
702 # RODC only - default, config, schema, app NCs
703 "msDS-hasFullReplicaNCs",
704 # Identifies if replica is coming, going, or stable
705 "msDS-HasInstantiatedNCs"
708 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
711 except ldb.LdbError, (enum, estr):
712 raise KCCError("Unable to find nTDSDSA NCs for (%s) - (%s)" %
713 (self.dsa_dnstr, estr))
715 # The table of NCs for the dsa we are searching
718 # We should get one response to our query here for
719 # the ntds that we requested
722 # Our response will contain a number of elements including
723 # the dn of the dsa as well as elements for each
724 # attribute (e.g. hasMasterNCs). Each of these elements
725 # is a dictonary list which we retrieve the keys for and
726 # then iterate over them
727 for k in res[0].keys():
731 # For each attribute type there will be one or more DNs
732 # listed. For instance DCs normally have 3 hasMasterNCs
734 for value in res[0][k]:
735 # Turn dn into a dsdb_Dn so we can use
736 # its methods to parse a binary DN
737 dsdn = dsdb_Dn(samdb, value)
738 flags = dsdn.get_binary_integer()
741 if not dnstr in tmp_table:
742 rep = NCReplica(self.dsa_dnstr, self.dsa_guid, dnstr)
743 tmp_table[dnstr] = rep
745 rep = tmp_table[dnstr]
747 if k == "msDS-HasInstantiatedNCs":
748 rep.set_instantiated_flags(flags)
751 rep.identify_by_dsa_attr(samdb, k)
753 # if we've identified the default domain NC
754 # then save its DN string
756 self.default_dnstr = dnstr
758 raise KCCError("No nTDSDSA NCs for (%s)" % self.dsa_dnstr)
760 # Assign our newly built NC replica table to this dsa
761 self.current_rep_table = tmp_table
763 def add_needed_replica(self, rep):
764 """Method to add a NC replica that "should be present" to the
767 self.needed_rep_table[rep.nc_dnstr] = rep
769 def load_connection_table(self, samdb):
770 """Method to load the nTDSConnections listed for DSA object.
772 :param samdb: database to query for DSA connection list
775 res = samdb.search(base=self.dsa_dnstr,
776 scope=ldb.SCOPE_SUBTREE,
777 expression="(objectClass=nTDSConnection)")
779 except ldb.LdbError, (enum, estr):
780 raise KCCError("Unable to find nTDSConnection for (%s) - (%s)" %
781 (self.dsa_dnstr, estr))
787 if dnstr in self.connect_table:
790 connect = NTDSConnection(dnstr)
792 connect.load_connection(samdb)
793 self.connect_table[dnstr] = connect
795 def commit_connections(self, samdb, ro=False):
796 """Method to commit any uncommitted nTDSConnections
797 modifications that are in our table. These would be
798 identified connections that are marked to be added or
801 :param samdb: database to commit DSA connection list to
802 :param ro: if (true) then peform internal operations but
803 do not write to the database (readonly)
807 for dnstr, connect in self.connect_table.items():
808 if connect.to_be_added:
809 connect.commit_added(samdb, ro)
811 if connect.to_be_modified:
812 connect.commit_modified(samdb, ro)
814 if connect.to_be_deleted:
815 connect.commit_deleted(samdb, ro)
816 delconn.append(dnstr)
818 # Now delete the connection from the table
819 for dnstr in delconn:
820 del self.connect_table[dnstr]
822 def add_connection(self, dnstr, connect):
823 assert dnstr not in self.connect_table
824 self.connect_table[dnstr] = connect
826 def get_connection_by_from_dnstr(self, from_dnstr):
827 """Scan DSA nTDSConnection table and return connection
828 with a "fromServer" dn string equivalent to method
831 :param from_dnstr: search for this from server entry
834 for connect in self.connect_table.values():
835 if connect.get_from_dnstr() == from_dnstr:
836 answer.append(connect)
840 def dumpstr_current_replica_table(self):
841 '''Debug dump string output of current replica table'''
842 return '\n'.join(str(x) for x in self.current_rep_table)
844 def dumpstr_needed_replica_table(self):
845 '''Debug dump string output of needed replica table'''
846 return '\n'.join(str(x) for x in self.needed_rep_table)
848 def dumpstr_connect_table(self):
849 '''Debug dump string output of connect table'''
850 return '\n'.join(str(x) for x in self.connect_table)
852 def new_connection(self, options, system_flags, transport, from_dnstr,
854 """Set up a new connection for the DSA based on input
855 parameters. Connection will be added to the DSA
856 connect_table and will be marked as "to be added" pending
857 a call to commit_connections()
859 dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
861 connect = NTDSConnection(dnstr)
862 connect.to_be_added = True
863 connect.enabled = True
864 connect.from_dnstr = from_dnstr
865 connect.options = options
866 connect.system_flags = system_flags
868 if transport is not None:
869 connect.transport_dnstr = transport.dnstr
870 connect.transport_guid = transport.guid
872 if sched is not None:
873 connect.schedule = sched
875 # Create schedule. Attribute valuse set according to MS-TECH
876 # intrasite connection creation document
877 connect.schedule = new_connection_schedule()
879 self.add_connection(dnstr, connect)
883 class NTDSConnection(object):
884 """Class defines a nTDSConnection found under a DSA
886 def __init__(self, dnstr):
891 self.to_be_added = False # new connection needs to be added
892 self.to_be_deleted = False # old connection needs to be deleted
893 self.to_be_modified = False
895 self.system_flags = 0
896 self.transport_dnstr = None
897 self.transport_guid = None
898 self.from_dnstr = None
902 '''Debug dump string output of NTDSConnection object'''
904 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
905 text = text + "\n\tenabled=%s" % self.enabled
906 text = text + "\n\tto_be_added=%s" % self.to_be_added
907 text = text + "\n\tto_be_deleted=%s" % self.to_be_deleted
908 text = text + "\n\tto_be_modified=%s" % self.to_be_modified
909 text = text + "\n\toptions=0x%08X" % self.options
910 text = text + "\n\tsystem_flags=0x%08X" % self.system_flags
911 text = text + "\n\twhenCreated=%d" % self.whenCreated
912 text = text + "\n\ttransport_dn=%s" % self.transport_dnstr
914 if self.guid is not None:
915 text = text + "\n\tguid=%s" % str(self.guid)
917 if self.transport_guid is not None:
918 text = text + "\n\ttransport_guid=%s" % str(self.transport_guid)
920 text = text + "\n\tfrom_dn=%s" % self.from_dnstr
922 if self.schedule is not None:
923 text += "\n\tschedule.size=%s" % self.schedule.size
924 text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
925 text += ("\n\tschedule.numberOfSchedules=%s" %
926 self.schedule.numberOfSchedules)
928 for i, header in enumerate(self.schedule.headerArray):
929 text += ("\n\tschedule.headerArray[%d].type=%d" %
931 text += ("\n\tschedule.headerArray[%d].offset=%d" %
933 text += "\n\tschedule.dataArray[%d].slots[ " % i
934 for slot in self.schedule.dataArray[i].slots:
935 text = text + "0x%X " % slot
940 def load_connection(self, samdb):
941 """Given a NTDSConnection object with an prior initialization
942 for the object's DN, search for the DN and load attributes
954 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
957 except ldb.LdbError, (enum, estr):
958 raise KCCError("Unable to find nTDSConnection for (%s) - (%s)" %
964 self.options = int(msg["options"][0])
966 if "enabledConnection" in msg:
967 if msg["enabledConnection"][0].upper().lstrip().rstrip() == "TRUE":
970 if "systemFlags" in msg:
971 self.system_flags = int(msg["systemFlags"][0])
975 misc.GUID(samdb.schema_format_value("objectGUID",
976 msg["objectGUID"][0]))
978 raise KCCError("Unable to find objectGUID in nTDSConnection "
979 "for (%s)" % (self.dnstr))
981 if "transportType" in msg:
982 dsdn = dsdb_Dn(samdb, msg["transportType"][0])
983 self.load_connection_transport(samdb, str(dsdn.dn))
985 if "schedule" in msg:
986 self.schedule = ndr_unpack(drsblobs.schedule, msg["schedule"][0])
988 if "whenCreated" in msg:
989 self.whenCreated = ldb.string_to_time(msg["whenCreated"][0])
991 if "fromServer" in msg:
992 dsdn = dsdb_Dn(samdb, msg["fromServer"][0])
993 self.from_dnstr = str(dsdn.dn)
994 assert self.from_dnstr is not None
996 def load_connection_transport(self, samdb, tdnstr):
997 """Given a NTDSConnection object which enumerates a transport
998 DN, load the transport information for the connection object
1000 :param tdnstr: transport DN to load
1002 attrs = ["objectGUID"]
1004 res = samdb.search(base=tdnstr,
1005 scope=ldb.SCOPE_BASE, attrs=attrs)
1007 except ldb.LdbError, (enum, estr):
1008 raise KCCError("Unable to find transport (%s) - (%s)" %
1011 if "objectGUID" in res[0]:
1013 self.transport_dnstr = tdnstr
1014 self.transport_guid = \
1015 misc.GUID(samdb.schema_format_value("objectGUID",
1016 msg["objectGUID"][0]))
1017 assert self.transport_dnstr is not None
1018 assert self.transport_guid is not None
1020 def commit_deleted(self, samdb, ro=False):
1021 """Local helper routine for commit_connections() which
1022 handles committed connections that are to be deleted from
1023 the database database
1025 assert self.to_be_deleted
1026 self.to_be_deleted = False
1028 # No database modification requested
1033 samdb.delete(self.dnstr)
1034 except ldb.LdbError, (enum, estr):
1035 raise KCCError("Could not delete nTDSConnection for (%s) - (%s)" %
1038 def commit_added(self, samdb, ro=False):
1039 """Local helper routine for commit_connections() which
1040 handles committed connections that are to be added to the
1043 assert self.to_be_added
1044 self.to_be_added = False
1046 # No database modification requested
1050 # First verify we don't have this entry to ensure nothing
1051 # is programatically amiss
1054 msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1058 except ldb.LdbError, (enum, estr):
1059 if enum != ldb.ERR_NO_SUCH_OBJECT:
1060 raise KCCError("Unable to search for (%s) - (%s)" %
1063 raise KCCError("nTDSConnection for (%s) already exists!" %
1071 # Prepare a message for adding to the samdb
1073 m.dn = ldb.Dn(samdb, self.dnstr)
1075 m["objectClass"] = \
1076 ldb.MessageElement("nTDSConnection", ldb.FLAG_MOD_ADD,
1078 m["showInAdvancedViewOnly"] = \
1079 ldb.MessageElement("TRUE", ldb.FLAG_MOD_ADD,
1080 "showInAdvancedViewOnly")
1081 m["enabledConnection"] = \
1082 ldb.MessageElement(enablestr, ldb.FLAG_MOD_ADD,
1083 "enabledConnection")
1085 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_ADD, "fromServer")
1087 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options")
1088 m["systemFlags"] = \
1089 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_ADD,
1092 if self.transport_dnstr is not None:
1093 m["transportType"] = \
1094 ldb.MessageElement(str(self.transport_dnstr), ldb.FLAG_MOD_ADD,
1097 if self.schedule is not None:
1099 ldb.MessageElement(ndr_pack(self.schedule),
1100 ldb.FLAG_MOD_ADD, "schedule")
1103 except ldb.LdbError, (enum, estr):
1104 raise KCCError("Could not add nTDSConnection for (%s) - (%s)" %
1107 def commit_modified(self, samdb, ro=False):
1108 """Local helper routine for commit_connections() which
1109 handles committed connections that are to be modified to the
1112 assert self.to_be_modified
1113 self.to_be_modified = False
1115 # No database modification requested
1119 # First verify we have this entry to ensure nothing
1120 # is programatically amiss
1122 # we don't use the search result, but it tests the status
1123 # of self.dnstr in the database.
1124 samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1126 except ldb.LdbError, (enum, estr):
1127 if enum == ldb.ERR_NO_SUCH_OBJECT:
1128 raise KCCError("nTDSConnection for (%s) doesn't exist!" %
1130 raise KCCError("Unable to search for (%s) - (%s)" %
1138 # Prepare a message for modifying the samdb
1140 m.dn = ldb.Dn(samdb, self.dnstr)
1142 m["enabledConnection"] = \
1143 ldb.MessageElement(enablestr, ldb.FLAG_MOD_REPLACE,
1144 "enabledConnection")
1146 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_REPLACE,
1149 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_REPLACE,
1151 m["systemFlags"] = \
1152 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_REPLACE,
1155 if self.transport_dnstr is not None:
1156 m["transportType"] = \
1157 ldb.MessageElement(str(self.transport_dnstr),
1158 ldb.FLAG_MOD_REPLACE, "transportType")
1160 m["transportType"] = \
1161 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "transportType")
1163 if self.schedule is not None:
1165 ldb.MessageElement(ndr_pack(self.schedule),
1166 ldb.FLAG_MOD_REPLACE, "schedule")
1169 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "schedule")
1172 except ldb.LdbError, (enum, estr):
1173 raise KCCError("Could not modify nTDSConnection for (%s) - (%s)" %
1176 def set_modified(self, truefalse):
1177 self.to_be_modified = truefalse
1179 def is_schedule_minimum_once_per_week(self):
1180 """Returns True if our schedule includes at least one
1181 replication interval within the week. False otherwise
1183 # replinfo schedule is None means "always", while
1184 # NTDSConnection schedule is None means "never".
1185 if self.schedule is None or self.schedule.dataArray[0] is None:
1188 for slot in self.schedule.dataArray[0].slots:
1189 if (slot & 0x0F) != 0x0:
1193 def is_equivalent_schedule(self, sched):
1194 """Returns True if our schedule is equivalent to the input
1195 comparison schedule.
1197 :param shed: schedule to compare to
1199 # There are 4 cases, where either self.schedule or sched can be None
1201 # | self. is None | self. is not None
1202 # --------------+-----------------+--------------------
1203 # sched is None | True | False
1204 # --------------+-----------------+--------------------
1205 # sched is not None | False | do calculations
1207 if self.schedule is None:
1208 return sched is None
1213 if ((self.schedule.size != sched.size or
1214 self.schedule.bandwidth != sched.bandwidth or
1215 self.schedule.numberOfSchedules != sched.numberOfSchedules)):
1218 for i, header in enumerate(self.schedule.headerArray):
1220 if self.schedule.headerArray[i].type != sched.headerArray[i].type:
1223 if self.schedule.headerArray[i].offset != \
1224 sched.headerArray[i].offset:
1227 for a, b in zip(self.schedule.dataArray[i].slots,
1228 sched.dataArray[i].slots):
1233 def is_rodc_topology(self):
1234 """Returns True if NTDS Connection specifies RODC
1237 if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
1241 def is_generated(self):
1242 """Returns True if NTDS Connection was generated by the
1243 KCC topology algorithm as opposed to set by the administrator
1245 if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0:
1249 def is_override_notify_default(self):
1250 """Returns True if NTDS Connection should override notify default
1252 if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0:
1256 def is_use_notify(self):
1257 """Returns True if NTDS Connection should use notify
1259 if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0:
1263 def is_twoway_sync(self):
1264 """Returns True if NTDS Connection should use twoway sync
1266 if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0:
1270 def is_intersite_compression_disabled(self):
1271 """Returns True if NTDS Connection intersite compression
1274 if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0:
1278 def is_user_owned_schedule(self):
1279 """Returns True if NTDS Connection has a user owned schedule
1281 if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0:
1285 def is_enabled(self):
1286 """Returns True if NTDS Connection is enabled
1290 def get_from_dnstr(self):
1291 '''Return fromServer dn string attribute'''
1292 return self.from_dnstr
1295 class Partition(NamingContext):
1296 """A naming context discovered thru Partitions DN of the config schema.
1298 This is a more specific form of NamingContext class (inheriting from that
1299 class) and it identifies unique attributes enumerated in the Partitions
1300 such as which nTDSDSAs are cross referenced for replicas
1302 def __init__(self, partstr):
1303 self.partstr = partstr
1305 self.system_flags = 0
1306 self.rw_location_list = []
1307 self.ro_location_list = []
1309 # We don't have enough info to properly
1310 # fill in the naming context yet. We'll get that
1311 # fully set up with load_partition().
1312 NamingContext.__init__(self, None)
1314 def load_partition(self, samdb):
1315 """Given a Partition class object that has been initialized with its
1316 partition dn string, load the partition from the sam database, identify
1317 the type of the partition (schema, domain, etc) and record the list of
1318 nTDSDSAs that appear in the cross reference attributes
1319 msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations.
1321 :param samdb: sam database to load partition from
1326 "msDS-NC-Replica-Locations",
1327 "msDS-NC-RO-Replica-Locations"]
1329 res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE,
1332 except ldb.LdbError, (enum, estr):
1333 raise KCCError("Unable to find partition for (%s) - (%s)" %
1334 (self.partstr, estr))
1336 for k in msg.keys():
1341 if msg[k][0].upper().lstrip().rstrip() == "TRUE":
1344 self.enabled = False
1347 if k == "systemFlags":
1348 self.system_flags = int(msg[k][0])
1351 for value in msg[k]:
1352 dsdn = dsdb_Dn(samdb, value)
1353 dnstr = str(dsdn.dn)
1356 self.nc_dnstr = dnstr
1359 if k == "msDS-NC-Replica-Locations":
1360 self.rw_location_list.append(dnstr)
1363 if k == "msDS-NC-RO-Replica-Locations":
1364 self.ro_location_list.append(dnstr)
1367 # Now identify what type of NC this partition
1369 self.identify_by_basedn(samdb)
1371 def is_enabled(self):
1372 """Returns True if partition is enabled
1374 return self.is_enabled
1376 def is_foreign(self):
1377 """Returns True if this is not an Active Directory NC in our
1378 forest but is instead something else (e.g. a foreign NC)
1380 if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0:
1385 def should_be_present(self, target_dsa):
1386 """Tests whether this partition should have an NC replica
1387 on the target dsa. This method returns a tuple of
1388 needed=True/False, ro=True/False, partial=True/False
1390 :param target_dsa: should NC be present on target dsa
1395 # If this is the config, schema, or default
1396 # domain NC for the target dsa then it should
1398 needed = (self.nc_type == NCType.config or
1399 self.nc_type == NCType.schema or
1400 (self.nc_type == NCType.domain and
1401 self.nc_dnstr == target_dsa.default_dnstr))
1403 # A writable replica of an application NC should be present
1404 # if there a cross reference to the target DSA exists. Depending
1405 # on whether the DSA is ro we examine which type of cross reference
1406 # to look for (msDS-NC-Replica-Locations or
1407 # msDS-NC-RO-Replica-Locations
1408 if self.nc_type == NCType.application:
1409 if target_dsa.is_ro():
1410 if target_dsa.dsa_dnstr in self.ro_location_list:
1413 if target_dsa.dsa_dnstr in self.rw_location_list:
1416 # If the target dsa is a gc then a partial replica of a
1417 # domain NC (other than the DSAs default domain) should exist
1418 # if there is also a cross reference for the DSA
1419 if (target_dsa.is_gc() and
1420 self.nc_type == NCType.domain and
1421 self.nc_dnstr != target_dsa.default_dnstr and
1422 (target_dsa.dsa_dnstr in self.ro_location_list or
1423 target_dsa.dsa_dnstr in self.rw_location_list)):
1427 # partial NCs are always readonly
1428 if needed and (target_dsa.is_ro() or partial):
1431 return needed, ro, partial
1434 '''Debug dump string output of class'''
1435 text = "%s" % NamingContext.__str__(self)
1436 text = text + "\n\tpartdn=%s" % self.partstr
1437 for k in self.rw_location_list:
1438 text = text + "\n\tmsDS-NC-Replica-Locations=%s" % k
1439 for k in self.ro_location_list:
1440 text = text + "\n\tmsDS-NC-RO-Replica-Locations=%s" % k
1445 """An individual site object discovered thru the configuration
1446 naming context. Contains all DSAs that exist within the site
1448 def __init__(self, site_dnstr, nt_now):
1449 self.site_dnstr = site_dnstr
1450 self.site_guid = None
1451 self.site_options = 0
1452 self.site_topo_generator = None
1453 self.site_topo_failover = 0 # appears to be in minutes
1455 self.rw_dsa_table = {}
1456 self.nt_now = nt_now
1458 def load_site(self, samdb):
1459 """Loads the NTDS Site Settings options attribute for the site
1460 as well as querying and loading all DSAs that appear within
1463 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1465 "interSiteTopologyFailover",
1466 "interSiteTopologyGenerator"]
1468 res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE,
1470 self_res = samdb.search(base=self.site_dnstr, scope=ldb.SCOPE_BASE,
1471 attrs=['objectGUID'])
1472 except ldb.LdbError, (enum, estr):
1473 raise KCCError("Unable to find site settings for (%s) - (%s)" %
1477 if "options" in msg:
1478 self.site_options = int(msg["options"][0])
1480 if "interSiteTopologyGenerator" in msg:
1481 self.site_topo_generator = \
1482 str(msg["interSiteTopologyGenerator"][0])
1484 if "interSiteTopologyFailover" in msg:
1485 self.site_topo_failover = int(msg["interSiteTopologyFailover"][0])
1488 if "objectGUID" in msg:
1489 self.site_guid = misc.GUID(samdb.schema_format_value("objectGUID",
1490 msg["objectGUID"][0]))
1492 self.load_all_dsa(samdb)
1494 def load_all_dsa(self, samdb):
1495 """Discover all nTDSDSA thru the sites entry and
1496 instantiate and load the DSAs. Each dsa is inserted
1497 into the dsa_table by dn string.
1500 res = samdb.search(self.site_dnstr,
1501 scope=ldb.SCOPE_SUBTREE,
1502 expression="(objectClass=nTDSDSA)")
1503 except ldb.LdbError, (enum, estr):
1504 raise KCCError("Unable to find nTDSDSAs - (%s)" % estr)
1510 if dnstr in self.dsa_table:
1513 dsa = DirectoryServiceAgent(dnstr)
1517 # Assign this dsa to my dsa table
1518 # and index by dsa dn
1519 self.dsa_table[dnstr] = dsa
1521 self.rw_dsa_table[dnstr] = dsa
1523 def get_dsa(self, dnstr):
1524 """Return a previously loaded DSA object by consulting
1525 the sites dsa_table for the provided DSA dn string
1527 :return: None if DSA doesn't exist
1529 return self.dsa_table.get(dnstr)
1531 def select_istg(self, samdb, mydsa, ro):
1532 """Determine if my DC should be an intersite topology
1533 generator. If my DC is the istg and is both a writeable
1534 DC and the database is opened in write mode then we perform
1535 an originating update to set the interSiteTopologyGenerator
1536 attribute in the NTDS Site Settings object. An RODC always
1537 acts as an ISTG for itself.
1539 # The KCC on an RODC always acts as an ISTG for itself
1541 mydsa.dsa_is_istg = True
1542 self.site_topo_generator = mydsa.dsa_dnstr
1545 c_rep = get_dsa_config_rep(mydsa)
1547 # Load repsFrom and replUpToDateVector if not already loaded
1548 # so we can get the current state of the config replica and
1549 # whether we are getting updates from the istg
1550 c_rep.load_repsFrom(samdb)
1552 c_rep.load_replUpToDateVector(samdb)
1554 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1555 # First, the KCC on a writable DC determines whether it acts
1556 # as an ISTG for its site
1558 # Let s be the object such that s!lDAPDisplayName = nTDSDSA
1559 # and classSchema in s!objectClass.
1561 # Let D be the sequence of objects o in the site of the local
1562 # DC such that o!objectCategory = s. D is sorted in ascending
1563 # order by objectGUID.
1565 # Which is a fancy way of saying "sort all the nTDSDSA objects
1566 # in the site by guid in ascending order". Place sorted list
1568 D_sort = sorted(self.rw_dsa_table.values(), cmp=sort_dsa_by_guid)
1570 # double word number of 100 nanosecond intervals since 1600s
1572 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1573 # if o!interSiteTopologyFailover is 0 or has no value.
1575 # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1576 # so it appears we have to turn f into the same interval
1578 # interSiteTopologyFailover (if set) appears to be in minutes
1579 # so we'll need to convert to senconds and then 100 nanosecond
1581 # XXX [MS-ADTS] 6.2.2.3.1 says it is seconds, not minutes.
1583 # 10,000,000 is number of 100 nanosecond intervals in a second
1584 if self.site_topo_failover == 0:
1585 f = 2 * 60 * 60 * 10000000
1587 f = self.site_topo_failover * 60 * 10000000
1589 # Let o be the site settings object for the site of the local
1590 # DC, or NULL if no such o exists.
1591 d_dsa = self.dsa_table.get(self.site_topo_generator)
1593 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1594 # If o != NULL and o!interSiteTopologyGenerator is not the
1595 # nTDSDSA object for the local DC and
1596 # o!interSiteTopologyGenerator is an element dj of sequence D:
1598 if d_dsa is not None and d_dsa is not mydsa:
1599 # From MS-ADTS 6.2.2.3.1 ISTG Selection:
1600 # Let c be the cursor in the replUpToDateVector variable
1601 # associated with the NC replica of the config NC such
1602 # that c.uuidDsa = dj!invocationId. If no such c exists
1603 # (No evidence of replication from current ITSG):
1607 # Else if the current time < c.timeLastSyncSuccess - f
1608 # (Evidence of time sync problem on current ISTG):
1612 # Else (Evidence of replication from current ITSG):
1614 # Let t = c.timeLastSyncSuccess.
1616 # last_success appears to be a double word containing
1617 # number of 100 nanosecond intervals since the 1600s
1618 j_idx = D_sort.index(d_dsa)
1621 for cursor in c_rep.rep_replUpToDateVector_cursors:
1622 if d_dsa.dsa_ivid == cursor.source_dsa_invocation_id:
1630 #XXX doc says current time < c.timeLastSyncSuccess - f
1631 # which is true only if f is negative or clocks are wrong.
1632 # f is not negative in the default case (2 hours).
1633 elif self.nt_now - cursor.last_sync_success > f:
1638 t_time = cursor.last_sync_success
1640 # Otherwise (Nominate local DC as ISTG):
1641 # Let i be the integer such that di is the nTDSDSA
1642 # object for the local DC.
1643 # Let t = the current time.
1645 i_idx = D_sort.index(mydsa)
1646 t_time = self.nt_now
1648 # Compute a function that maintains the current ISTG if
1649 # it is alive, cycles through other candidates if not.
1651 # Let k be the integer (i + ((current time - t) /
1652 # o!interSiteTopologyFailover)) MOD |D|.
1654 # Note: We don't want to divide by zero here so they must
1655 # have meant "f" instead of "o!interSiteTopologyFailover"
1656 k_idx = (i_idx + ((self.nt_now - t_time) / f)) % len(D_sort)
1658 # The local writable DC acts as an ISTG for its site if and
1659 # only if dk is the nTDSDSA object for the local DC. If the
1660 # local DC does not act as an ISTG, the KCC skips the
1661 # remainder of this task.
1662 d_dsa = D_sort[k_idx]
1663 d_dsa.dsa_is_istg = True
1665 # Update if we are the ISTG, otherwise return
1666 if d_dsa is not mydsa:
1670 if self.site_topo_generator == mydsa.dsa_dnstr:
1673 self.site_topo_generator = mydsa.dsa_dnstr
1675 # If readonly database then do not perform a
1680 # Perform update to the samdb
1681 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1684 m.dn = ldb.Dn(samdb, ssdn)
1686 m["interSiteTopologyGenerator"] = \
1687 ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
1688 "interSiteTopologyGenerator")
1692 except ldb.LdbError, estr:
1694 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1698 def is_intrasite_topology_disabled(self):
1699 '''Returns True if intra-site topology is disabled for site'''
1700 return (self.site_options &
1701 dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0
1703 def is_intersite_topology_disabled(self):
1704 '''Returns True if inter-site topology is disabled for site'''
1705 return ((self.site_options &
1706 dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED)
1709 def is_random_bridgehead_disabled(self):
1710 '''Returns True if selection of random bridgehead is disabled'''
1711 return (self.site_options &
1712 dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0
1714 def is_detect_stale_disabled(self):
1715 '''Returns True if detect stale is disabled for site'''
1716 return (self.site_options &
1717 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0
1719 def is_cleanup_ntdsconn_disabled(self):
1720 '''Returns True if NTDS Connection cleanup is disabled for site'''
1721 return (self.site_options &
1722 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0
1724 def same_site(self, dsa):
1725 '''Return True if dsa is in this site'''
1726 if self.get_dsa(dsa.dsa_dnstr):
1730 def is_rodc_site(self):
1731 if len(self.dsa_table) > 0 and len(self.rw_dsa_table) == 0:
1736 '''Debug dump string output of class'''
1737 text = "%s:" % self.__class__.__name__
1738 text = text + "\n\tdn=%s" % self.site_dnstr
1739 text = text + "\n\toptions=0x%X" % self.site_options
1740 text = text + "\n\ttopo_generator=%s" % self.site_topo_generator
1741 text = text + "\n\ttopo_failover=%d" % self.site_topo_failover
1742 for key, dsa in self.dsa_table.items():
1743 text = text + "\n%s" % dsa
1747 class GraphNode(object):
1748 """A graph node describing a set of edges that should be directed to it.
1750 Each edge is a connection for a particular naming context replica directed
1751 from another node in the forest to this node.
1754 def __init__(self, dsa_dnstr, max_node_edges):
1755 """Instantiate the graph node according to a DSA dn string
1757 :param max_node_edges: maximum number of edges that should ever
1758 be directed to the node
1760 self.max_edges = max_node_edges
1761 self.dsa_dnstr = dsa_dnstr
1765 text = "%s:" % self.__class__.__name__
1766 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
1767 text = text + "\n\tmax_edges=%d" % self.max_edges
1769 for i, edge in enumerate(self.edge_from):
1770 if isinstance(edge, str):
1771 text += "\n\tedge_from[%d]=%s" % (i, edge)
1775 def add_edge_from(self, from_dsa_dnstr):
1776 """Add an edge from the dsa to our graph nodes edge from list
1778 :param from_dsa_dnstr: the dsa that the edge emanates from
1780 assert isinstance(from_dsa_dnstr, str)
1782 # No edges from myself to myself
1783 if from_dsa_dnstr == self.dsa_dnstr:
1785 # Only one edge from a particular node
1786 if from_dsa_dnstr in self.edge_from:
1788 # Not too many edges
1789 if len(self.edge_from) >= self.max_edges:
1791 self.edge_from.append(from_dsa_dnstr)
1794 def add_edges_from_connections(self, dsa):
1795 """For each nTDSConnection object associated with a particular
1796 DSA, we test if it implies an edge to this graph node (i.e.
1797 the "fromServer" attribute). If it does then we add an
1798 edge from the server unless we are over the max edges for this
1801 :param dsa: dsa with a dnstr equivalent to his graph node
1803 for connect in dsa.connect_table.values():
1804 self.add_edge_from(connect.from_dnstr)
1806 def add_connections_from_edges(self, dsa, transport):
1807 """For each edge directed to this graph node, ensure there
1808 is a corresponding nTDSConnection object in the dsa.
1810 for edge_dnstr in self.edge_from:
1811 connections = dsa.get_connection_by_from_dnstr(edge_dnstr)
1813 # For each edge directed to the NC replica that
1814 # "should be present" on the local DC, the KCC determines
1815 # whether an object c exists such that:
1817 # c is a child of the DC's nTDSDSA object.
1818 # c.objectCategory = nTDSConnection
1820 # Given the NC replica ri from which the edge is directed,
1821 # c.fromServer is the dsname of the nTDSDSA object of
1822 # the DC on which ri "is present".
1824 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1827 for connect in connections:
1828 if connect.is_rodc_topology():
1835 # if no such object exists then the KCC adds an object
1836 # c with the following attributes
1838 # Generate a new dnstr for this nTDSConnection
1839 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1840 flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1841 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1843 dsa.new_connection(opt, flags, transport, edge_dnstr, None)
1845 def has_sufficient_edges(self):
1846 '''Return True if we have met the maximum "from edges" criteria'''
1847 if len(self.edge_from) >= self.max_edges:
1852 class Transport(object):
1853 """Class defines a Inter-site transport found under Sites
1856 def __init__(self, dnstr):
1861 self.address_attr = None
1862 self.bridgehead_list = []
1865 '''Debug dump string output of Transport object'''
1867 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
1868 text = text + "\n\tguid=%s" % str(self.guid)
1869 text = text + "\n\toptions=%d" % self.options
1870 text = text + "\n\taddress_attr=%s" % self.address_attr
1871 text = text + "\n\tname=%s" % self.name
1872 for dnstr in self.bridgehead_list:
1873 text = text + "\n\tbridgehead_list=%s" % dnstr
1877 def load_transport(self, samdb):
1878 """Given a Transport object with an prior initialization
1879 for the object's DN, search for the DN and load attributes
1882 attrs = ["objectGUID",
1885 "bridgeheadServerListBL",
1886 "transportAddressAttribute"]
1888 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
1891 except ldb.LdbError, (enum, estr):
1892 raise KCCError("Unable to find Transport for (%s) - (%s)" %
1896 self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
1897 msg["objectGUID"][0]))
1899 if "options" in msg:
1900 self.options = int(msg["options"][0])
1902 if "transportAddressAttribute" in msg:
1903 self.address_attr = str(msg["transportAddressAttribute"][0])
1906 self.name = str(msg["name"][0])
1908 if "bridgeheadServerListBL" in msg:
1909 for value in msg["bridgeheadServerListBL"]:
1910 dsdn = dsdb_Dn(samdb, value)
1911 dnstr = str(dsdn.dn)
1912 if dnstr not in self.bridgehead_list:
1913 self.bridgehead_list.append(dnstr)
1916 class RepsFromTo(object):
1917 """Class encapsulation of the NDR repsFromToBlob.
1919 Removes the necessity of external code having to
1920 understand about other_info or manipulation of
1923 def __init__(self, nc_dnstr=None, ndr_blob=None):
1925 self.__dict__['to_be_deleted'] = False
1926 self.__dict__['nc_dnstr'] = nc_dnstr
1927 self.__dict__['update_flags'] = 0x0
1928 # XXX the following sounds dubious and/or better solved
1929 # elsewhere, but lets leave it for now. In particular, there
1930 # seems to be no reason for all the non-ndr generated
1931 # attributes to be handled in the round about way (e.g.
1932 # self.__dict__['to_be_deleted'] = False above). On the other
1933 # hand, it all seems to work. Hooray! Hands off!.
1937 # There is a very subtle bug here with python
1938 # and our NDR code. If you assign directly to
1939 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1940 # then a proper python GC reference count is not
1943 # To work around this we maintain an internal
1944 # reference to "dns_name(x)" and "other_info" elements
1945 # of repsFromToBlob. This internal reference
1946 # is hidden within this class but it is why you
1947 # see statements like this below:
1949 # self.__dict__['ndr_blob'].ctr.other_info = \
1950 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1952 # That would appear to be a redundant assignment but
1953 # it is necessary to hold a proper python GC reference
1955 if ndr_blob is None:
1956 self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
1957 self.__dict__['ndr_blob'].version = 0x1
1958 self.__dict__['dns_name1'] = None
1959 self.__dict__['dns_name2'] = None
1961 self.__dict__['ndr_blob'].ctr.other_info = \
1962 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1965 self.__dict__['ndr_blob'] = ndr_blob
1966 self.__dict__['other_info'] = ndr_blob.ctr.other_info
1968 if ndr_blob.version == 0x1:
1969 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
1970 self.__dict__['dns_name2'] = None
1972 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
1973 self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
1976 '''Debug dump string output of class'''
1978 text = "%s:" % self.__class__.__name__
1979 text += "\n\tdnstr=%s" % self.nc_dnstr
1980 text += "\n\tupdate_flags=0x%X" % self.update_flags
1981 text += "\n\tversion=%d" % self.version
1982 text += "\n\tsource_dsa_obj_guid=%s" % self.source_dsa_obj_guid
1983 text += ("\n\tsource_dsa_invocation_id=%s" %
1984 self.source_dsa_invocation_id)
1985 text += "\n\ttransport_guid=%s" % self.transport_guid
1986 text += "\n\treplica_flags=0x%X" % self.replica_flags
1987 text += ("\n\tconsecutive_sync_failures=%d" %
1988 self.consecutive_sync_failures)
1989 text += "\n\tlast_success=%s" % self.last_success
1990 text += "\n\tlast_attempt=%s" % self.last_attempt
1991 text += "\n\tdns_name1=%s" % self.dns_name1
1992 text += "\n\tdns_name2=%s" % self.dns_name2
1993 text += "\n\tschedule[ "
1994 for slot in self.schedule:
1995 text += "0x%X " % slot
2000 def __setattr__(self, item, value):
2001 """Set an attribute and chyange update flag.
2003 Be aware that setting any RepsFromTo attribute will set the
2004 drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS update flag.
2006 if item in ['schedule', 'replica_flags', 'transport_guid',
2007 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2008 'consecutive_sync_failures', 'last_success',
2011 if item in ['replica_flags']:
2012 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
2013 elif item in ['schedule']:
2014 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
2016 setattr(self.__dict__['ndr_blob'].ctr, item, value)
2018 elif item in ['dns_name1']:
2019 self.__dict__['dns_name1'] = value
2021 if self.__dict__['ndr_blob'].version == 0x1:
2022 self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
2023 self.__dict__['dns_name1']
2025 self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
2026 self.__dict__['dns_name1']
2028 elif item in ['dns_name2']:
2029 self.__dict__['dns_name2'] = value
2031 if self.__dict__['ndr_blob'].version == 0x1:
2032 raise AttributeError(item)
2034 self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
2035 self.__dict__['dns_name2']
2037 elif item in ['nc_dnstr']:
2038 self.__dict__['nc_dnstr'] = value
2040 elif item in ['to_be_deleted']:
2041 self.__dict__['to_be_deleted'] = value
2043 elif item in ['version']:
2044 raise AttributeError("Attempt to set readonly attribute %s" % item)
2046 raise AttributeError("Unknown attribute %s" % item)
2048 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
2050 def __getattr__(self, item):
2051 """Overload of RepsFromTo attribute retrieval.
2053 Allows external code to ignore substructures within the blob
2055 if item in ['schedule', 'replica_flags', 'transport_guid',
2056 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2057 'consecutive_sync_failures', 'last_success',
2059 return getattr(self.__dict__['ndr_blob'].ctr, item)
2061 elif item in ['version']:
2062 return self.__dict__['ndr_blob'].version
2064 elif item in ['dns_name1']:
2065 if self.__dict__['ndr_blob'].version == 0x1:
2066 return self.__dict__['ndr_blob'].ctr.other_info.dns_name
2068 return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
2070 elif item in ['dns_name2']:
2071 if self.__dict__['ndr_blob'].version == 0x1:
2072 raise AttributeError(item)
2074 return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
2076 elif item in ['to_be_deleted']:
2077 return self.__dict__['to_be_deleted']
2079 elif item in ['nc_dnstr']:
2080 return self.__dict__['nc_dnstr']
2082 elif item in ['update_flags']:
2083 return self.__dict__['update_flags']
2085 raise AttributeError("Unknown attribute %s" % item)
2087 def is_modified(self):
2088 return (self.update_flags != 0x0)
2090 def set_unmodified(self):
2091 self.__dict__['update_flags'] = 0x0
2094 class SiteLink(object):
2095 """Class defines a site link found under sites
2098 def __init__(self, dnstr):
2101 self.system_flags = 0
2103 self.schedule = None
2104 self.interval = None
2108 '''Debug dump string output of Transport object'''
2110 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
2111 text = text + "\n\toptions=%d" % self.options
2112 text = text + "\n\tsystem_flags=%d" % self.system_flags
2113 text = text + "\n\tcost=%d" % self.cost
2114 text = text + "\n\tinterval=%s" % self.interval
2116 if self.schedule is not None:
2117 text += "\n\tschedule.size=%s" % self.schedule.size
2118 text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
2119 text += ("\n\tschedule.numberOfSchedules=%s" %
2120 self.schedule.numberOfSchedules)
2122 for i, header in enumerate(self.schedule.headerArray):
2123 text += ("\n\tschedule.headerArray[%d].type=%d" %
2125 text += ("\n\tschedule.headerArray[%d].offset=%d" %
2127 text = text + "\n\tschedule.dataArray[%d].slots[ " % i
2128 for slot in self.schedule.dataArray[i].slots:
2129 text = text + "0x%X " % slot
2132 for dnstr in self.site_list:
2133 text = text + "\n\tsite_list=%s" % dnstr
2136 def load_sitelink(self, samdb):
2137 """Given a siteLink object with an prior initialization
2138 for the object's DN, search for the DN and load attributes
2148 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
2149 attrs=attrs, controls=['extended_dn:0'])
2151 except ldb.LdbError, (enum, estr):
2152 raise KCCError("Unable to find SiteLink for (%s) - (%s)" %
2157 if "options" in msg:
2158 self.options = int(msg["options"][0])
2160 if "systemFlags" in msg:
2161 self.system_flags = int(msg["systemFlags"][0])
2164 self.cost = int(msg["cost"][0])
2166 if "replInterval" in msg:
2167 self.interval = int(msg["replInterval"][0])
2169 if "siteList" in msg:
2170 for value in msg["siteList"]:
2171 dsdn = dsdb_Dn(samdb, value)
2172 guid = misc.GUID(dsdn.dn.get_extended_component('GUID'))
2173 if guid not in self.site_list:
2174 self.site_list.append(guid)
2176 if "schedule" in msg:
2177 self.schedule = ndr_unpack(drsblobs.schedule, value)
2179 self.schedule = new_connection_schedule()
2182 class KCCFailedObject(object):
2183 def __init__(self, uuid, failure_count, time_first_failure,
2184 last_result, dns_name):
2186 self.failure_count = failure_count
2187 self.time_first_failure = time_first_failure
2188 self.last_result = last_result
2189 self.dns_name = dns_name
2192 ##################################################
2193 # Global Functions and Variables
2194 ##################################################
2196 def get_dsa_config_rep(dsa):
2197 # Find configuration NC replica for the DSA
2198 for c_rep in dsa.current_rep_table.values():
2199 if c_rep.is_config():
2202 raise KCCError("Unable to find config NC replica for (%s)" %
2206 def sort_dsa_by_guid(dsa1, dsa2):
2207 "use ndr_pack for GUID comparison, as appears correct in some places"""
2208 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
2211 def new_connection_schedule():
2212 """Create a default schedule for an NTDSConnection or Sitelink. This
2213 is packed differently from the repltimes schedule used elsewhere
2214 in KCC (where the 168 nibbles are packed into 84 bytes).
2216 # 168 byte instances of the 0x01 value. The low order 4 bits
2217 # of the byte equate to 15 minute intervals within a single hour.
2218 # There are 168 bytes because there are 168 hours in a full week
2219 # Effectively we are saying to perform replication at the end of
2220 # each hour of the week
2221 schedule = drsblobs.schedule()
2224 schedule.bandwidth = 0
2225 schedule.numberOfSchedules = 1
2227 header = drsblobs.scheduleHeader()
2231 schedule.headerArray = [header]
2233 data = drsblobs.scheduleSlots()
2234 data.slots = [0x01] * 168
2236 schedule.dataArray = [data]