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 as e:
88 raise KCCError("Unable to find naming context (%s) - (%s)" %
89 (self.nc_dnstr, estr))
91 if "objectGUID" in msg:
92 self.nc_guid = misc.GUID(samdb.schema_format_value("objectGUID",
93 msg["objectGUID"][0]))
94 if "objectSid" in msg:
95 self.nc_sid = msg["objectSid"][0]
97 assert self.nc_guid is not None
100 '''Return True if NC is config'''
101 assert self.nc_type != NCType.unknown
102 return self.nc_type == NCType.config
104 def identify_by_basedn(self, samdb):
105 """Given an NC object, identify what type is is thru
106 the samdb basedn strings and NC sid value
108 # Invoke loader to initialize guid and more
109 # importantly sid value (sid is used to identify
111 if self.nc_guid is None:
114 # We check against schema and config because they
115 # will be the same for all nTDSDSAs in the forest.
116 # That leaves the domain NCs which can be identified
117 # by sid and application NCs as the last identified
118 if self.nc_dnstr == str(samdb.get_schema_basedn()):
119 self.nc_type = NCType.schema
120 elif self.nc_dnstr == str(samdb.get_config_basedn()):
121 self.nc_type = NCType.config
122 elif self.nc_sid is not None:
123 self.nc_type = NCType.domain
125 self.nc_type = NCType.application
127 def identify_by_dsa_attr(self, samdb, attr):
128 """Given an NC which has been discovered thru the
129 nTDSDSA database object, determine what type of NC
130 it is (i.e. schema, config, domain, application) via
131 the use of the schema attribute under which the NC
134 :param attr: attr of nTDSDSA object where NC DN appears
136 # If the NC is listed under msDS-HasDomainNCs then
137 # this can only be a domain NC and it is our default
138 # domain for this dsa
139 if attr == "msDS-HasDomainNCs":
140 self.nc_type = NCType.domain
142 # If the NC is listed under hasPartialReplicaNCs
143 # this is only a domain NC
144 elif attr == "hasPartialReplicaNCs":
145 self.nc_type = NCType.domain
147 # NCs listed under hasMasterNCs are either
148 # default domain, schema, or config. We
149 # utilize the identify_by_basedn() to
151 elif attr == "hasMasterNCs":
152 self.identify_by_basedn(samdb)
154 # Still unknown (unlikely) but for completeness
155 # and for finally identifying application NCs
156 if self.nc_type == NCType.unknown:
157 self.identify_by_basedn(samdb)
160 class NCReplica(NamingContext):
161 """Naming context replica that is relative to a specific DSA.
163 This is a more specific form of NamingContext class (inheriting from that
164 class) and it identifies unique attributes of the DSA's replica for a NC.
167 def __init__(self, dsa, nc_dnstr):
168 """Instantiate a Naming Context Replica
170 :param dsa_guid: GUID of DSA where replica appears
171 :param nc_dnstr: NC dn string
173 self.rep_dsa_dnstr = dsa.dsa_dnstr
174 self.rep_dsa_guid = dsa.dsa_guid
175 self.rep_default = False # replica for DSA's default domain
176 self.rep_partial = False
178 self.rep_instantiated_flags = 0
180 self.rep_fsmo_role_owner = None
183 self.rep_repsFrom = []
188 # The (is present) test is a combination of being
189 # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or
190 # hasPartialReplicaNCs) as well as its replica flags found
191 # thru the msDS-HasInstantiatedNCs. If the NC replica meets
192 # the first enumeration test then this flag is set true
193 self.rep_present_criteria_one = False
195 # Call my super class we inherited from
196 NamingContext.__init__(self, nc_dnstr)
199 '''Debug dump string output of class'''
200 text = "%s:" % self.__class__.__name__
201 text = text + "\n\tdsa_dnstr=%s" % self.rep_dsa_dnstr
202 text = text + "\n\tdsa_guid=%s" % self.rep_dsa_guid
203 text = text + "\n\tdefault=%s" % self.rep_default
204 text = text + "\n\tro=%s" % self.rep_ro
205 text = text + "\n\tpartial=%s" % self.rep_partial
206 text = text + "\n\tpresent=%s" % self.is_present()
207 text = text + "\n\tfsmo_role_owner=%s" % self.rep_fsmo_role_owner
209 for rep in self.rep_repsFrom:
210 text = text + "\n%s" % rep
212 for rep in self.rep_repsTo:
213 text = text + "\n%s" % rep
215 return "%s\n%s" % (NamingContext.__str__(self), text)
217 def set_instantiated_flags(self, flags=0):
218 '''Set or clear NC replica instantiated flags'''
219 self.rep_instantiated_flags = flags
221 def identify_by_dsa_attr(self, samdb, attr):
222 """Given an NC which has been discovered thru the
223 nTDSDSA database object, determine what type of NC
224 replica it is (i.e. partial, read only, default)
226 :param attr: attr of nTDSDSA object where NC DN appears
228 # If the NC was found under hasPartialReplicaNCs
229 # then a partial replica at this dsa
230 if attr == "hasPartialReplicaNCs":
231 self.rep_partial = True
232 self.rep_present_criteria_one = True
234 # If the NC is listed under msDS-HasDomainNCs then
235 # this can only be a domain NC and it is the DSA's
237 elif attr == "msDS-HasDomainNCs":
238 self.rep_default = True
240 # NCs listed under hasMasterNCs are either
241 # default domain, schema, or config. We check
242 # against schema and config because they will be
243 # the same for all nTDSDSAs in the forest. That
244 # leaves the default domain NC remaining which
245 # may be different for each nTDSDSAs (and thus
246 # we don't compare agains this samdb's default
248 elif attr == "hasMasterNCs":
249 self.rep_present_criteria_one = True
251 if self.nc_dnstr != str(samdb.get_schema_basedn()) and \
252 self.nc_dnstr != str(samdb.get_config_basedn()):
253 self.rep_default = True
256 elif attr == "msDS-hasFullReplicaNCs":
257 self.rep_present_criteria_one = True
261 elif attr == "msDS-hasMasterNCs":
262 self.rep_present_criteria_one = True
265 # Now use this DSA attribute to identify the naming
266 # context type by calling the super class method
268 NamingContext.identify_by_dsa_attr(self, samdb, attr)
270 def is_default(self):
271 """Whether this is a default domain for the dsa that this NC appears on
273 return self.rep_default
276 '''Return True if NC replica is read only'''
279 def is_partial(self):
280 '''Return True if NC replica is partial'''
281 return self.rep_partial
283 def is_present(self):
284 """Given an NC replica which has been discovered thru the
285 nTDSDSA database object and populated with replica flags
286 from the msDS-HasInstantiatedNCs; return whether the NC
287 replica is present (true) or if the IT_NC_GOING flag is
288 set then the NC replica is not present (false)
290 if self.rep_present_criteria_one and \
291 self.rep_instantiated_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0:
295 def load_repsFrom(self, samdb):
296 """Given an NC replica which has been discovered thru the nTDSDSA
297 database object, load the repsFrom attribute for the local replica.
298 held by my dsa. The repsFrom attribute is not replicated so this
299 attribute is relative only to the local DSA that the samdb exists on
302 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
305 except ldb.LdbError as e1:
306 (enum, estr) = e1.args
307 raise KCCError("Unable to find NC for (%s) - (%s)" %
308 (self.nc_dnstr, estr))
312 # Possibly no repsFrom if this is a singleton DC
313 if "repsFrom" in msg:
314 for value in msg["repsFrom"]:
315 rep = RepsFromTo(self.nc_dnstr,
316 ndr_unpack(drsblobs.repsFromToBlob, value))
317 self.rep_repsFrom.append(rep)
319 def commit_repsFrom(self, samdb, ro=False):
320 """Commit repsFrom to the database"""
322 # XXX - This is not truly correct according to the MS-TECH
323 # docs. To commit a repsFrom we should be using RPCs
324 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
325 # IDL_DRSReplicaDel to affect a repsFrom change.
327 # Those RPCs are missing in samba, so I'll have to
328 # implement them to get this to more accurately
329 # reflect the reference docs. As of right now this
330 # commit to the database will work as its what the
336 for repsFrom in self.rep_repsFrom:
338 # Leave out any to be deleted from
339 # replacement list. Build a list
340 # of to be deleted reps which we will
341 # remove from rep_repsFrom list below
342 if repsFrom.to_be_deleted:
343 delreps.append(repsFrom)
347 if repsFrom.is_modified():
348 repsFrom.set_unmodified()
351 # current (unmodified) elements also get
352 # appended here but no changes will occur
353 # unless something is "to be modified" or
355 newreps.append(ndr_pack(repsFrom.ndr_blob))
357 # Now delete these from our list of rep_repsFrom
358 for repsFrom in delreps:
359 self.rep_repsFrom.remove(repsFrom)
362 # Nothing to do if no reps have been modified or
363 # need to be deleted or input option has informed
364 # us to be "readonly" (ro). Leave database
370 m.dn = ldb.Dn(samdb, self.nc_dnstr)
373 ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsFrom")
378 except ldb.LdbError as estr:
379 raise KCCError("Could not set repsFrom for (%s) - (%s)" %
380 (self.nc_dnstr, estr))
382 def load_replUpToDateVector(self, samdb):
383 """Given an NC replica which has been discovered thru the nTDSDSA
384 database object, load the replUpToDateVector attribute for the
385 local replica. held by my dsa. The replUpToDateVector
386 attribute is not replicated so this attribute is relative only
387 to the local DSA that the samdb exists on
391 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
392 attrs=["replUpToDateVector"])
394 except ldb.LdbError as e2:
395 (enum, estr) = e2.args
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 as e3:
430 (enum, estr) = e3.args
431 raise KCCError("Unable to find NC for (%s) - (%s)" %
432 (self.nc_dnstr, estr))
436 # Possibly no fSMORoleOwner
437 if "fSMORoleOwner" in msg:
438 self.rep_fsmo_role_owner = msg["fSMORoleOwner"]
440 def is_fsmo_role_owner(self, dsa_dnstr):
441 if self.rep_fsmo_role_owner is not None and \
442 self.rep_fsmo_role_owner == dsa_dnstr:
446 def load_repsTo(self, samdb):
447 """Given an NC replica which has been discovered thru the nTDSDSA
448 database object, load the repsTo attribute for the local replica.
449 held by my dsa. The repsTo attribute is not replicated so this
450 attribute is relative only to the local DSA that the samdb exists on
452 This is responsible for push replication, not scheduled pull
453 replication. Not to be confused for repsFrom.
456 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
459 except ldb.LdbError as e4:
460 (enum, estr) = e4.args
461 raise KCCError("Unable to find NC for (%s) - (%s)" %
462 (self.nc_dnstr, estr))
466 # Possibly no repsTo if this is a singleton DC
468 for value in msg["repsTo"]:
469 rep = RepsFromTo(self.nc_dnstr,
470 ndr_unpack(drsblobs.repsFromToBlob, value))
471 self.rep_repsTo.append(rep)
473 def commit_repsTo(self, samdb, ro=False):
474 """Commit repsTo to the database"""
476 # XXX - This is not truly correct according to the MS-TECH
477 # docs. To commit a repsTo we should be using RPCs
478 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
479 # IDL_DRSReplicaDel to affect a repsTo change.
481 # Those RPCs are missing in samba, so I'll have to
482 # implement them to get this to more accurately
483 # reflect the reference docs. As of right now this
484 # commit to the database will work as its what the
490 for repsTo in self.rep_repsTo:
492 # Leave out any to be deleted from
493 # replacement list. Build a list
494 # of to be deleted reps which we will
495 # remove from rep_repsTo list below
496 if repsTo.to_be_deleted:
497 delreps.append(repsTo)
501 if repsTo.is_modified():
502 repsTo.set_unmodified()
505 # current (unmodified) elements also get
506 # appended here but no changes will occur
507 # unless something is "to be modified" or
509 newreps.append(ndr_pack(repsTo.ndr_blob))
511 # Now delete these from our list of rep_repsTo
512 for repsTo in delreps:
513 self.rep_repsTo.remove(repsTo)
516 # Nothing to do if no reps have been modified or
517 # need to be deleted or input option has informed
518 # us to be "readonly" (ro). Leave database
524 m.dn = ldb.Dn(samdb, self.nc_dnstr)
527 ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsTo")
532 except ldb.LdbError as estr:
533 raise KCCError("Could not set repsTo for (%s) - (%s)" %
534 (self.nc_dnstr, estr))
537 class DirectoryServiceAgent(object):
539 def __init__(self, dsa_dnstr):
540 """Initialize DSA class.
542 Class is subsequently fully populated by calling the load_dsa() method
544 :param dsa_dnstr: DN of the nTDSDSA
546 self.dsa_dnstr = dsa_dnstr
549 self.dsa_is_ro = False
550 self.dsa_is_istg = False
552 self.dsa_behavior = 0
553 self.default_dnstr = None # default domain dn string for dsa
555 # NCReplicas for this dsa that are "present"
556 # Indexed by DN string of naming context
557 self.current_rep_table = {}
559 # NCReplicas for this dsa that "should be present"
560 # Indexed by DN string of naming context
561 self.needed_rep_table = {}
563 # NTDSConnections for this dsa. These are current
564 # valid connections that are committed or pending a commit
565 # in the database. Indexed by DN string of connection
566 self.connect_table = {}
569 '''Debug dump string output of class'''
571 text = "%s:" % self.__class__.__name__
572 if self.dsa_dnstr is not None:
573 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
574 if self.dsa_guid is not None:
575 text = text + "\n\tdsa_guid=%s" % str(self.dsa_guid)
576 if self.dsa_ivid is not None:
577 text = text + "\n\tdsa_ivid=%s" % str(self.dsa_ivid)
579 text = text + "\n\tro=%s" % self.is_ro()
580 text = text + "\n\tgc=%s" % self.is_gc()
581 text = text + "\n\tistg=%s" % self.is_istg()
583 text = text + "\ncurrent_replica_table:"
584 text = text + "\n%s" % self.dumpstr_current_replica_table()
585 text = text + "\nneeded_replica_table:"
586 text = text + "\n%s" % self.dumpstr_needed_replica_table()
587 text = text + "\nconnect_table:"
588 text = text + "\n%s" % self.dumpstr_connect_table()
592 def get_current_replica(self, nc_dnstr):
593 return self.current_rep_table.get(nc_dnstr)
596 '''Returns True if dsa is intersite topology generator for it's site'''
597 # The KCC on an RODC always acts as an ISTG for itself
598 return self.dsa_is_istg or self.dsa_is_ro
601 '''Returns True if dsa a read only domain controller'''
602 return self.dsa_is_ro
605 '''Returns True if dsa hosts a global catalog'''
606 if (self.options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
610 def is_minimum_behavior(self, version):
611 """Is dsa at minimum windows level greater than or equal to (version)
613 :param version: Windows version to test against
614 (e.g. DS_DOMAIN_FUNCTION_2008)
616 if self.dsa_behavior >= version:
620 def is_translate_ntdsconn_disabled(self):
621 """Whether this allows NTDSConnection translation in its options."""
622 if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0:
626 def get_rep_tables(self):
627 """Return DSA current and needed replica tables
629 return self.current_rep_table, self.needed_rep_table
631 def get_parent_dnstr(self):
632 """Get the parent DN string of this object."""
633 head, sep, tail = self.dsa_dnstr.partition(',')
636 def load_dsa(self, samdb):
637 """Load a DSA from the samdb.
639 Prior initialization has given us the DN of the DSA that we are to
640 load. This method initializes all other attributes, including loading
641 the NC replica table for this DSA.
643 attrs = ["objectGUID",
647 "msDS-Behavior-Version"]
649 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
652 except ldb.LdbError as e5:
653 (enum, estr) = e5.args
654 raise KCCError("Unable to find nTDSDSA for (%s) - (%s)" %
655 (self.dsa_dnstr, estr))
658 self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID",
659 msg["objectGUID"][0]))
661 # RODCs don't originate changes and thus have no invocationId,
662 # therefore we must check for existence first
663 if "invocationId" in msg:
664 self.dsa_ivid = misc.GUID(samdb.schema_format_value("objectGUID",
665 msg["invocationId"][0]))
668 self.options = int(msg["options"][0])
670 if "msDS-isRODC" in msg and msg["msDS-isRODC"][0] == "TRUE":
671 self.dsa_is_ro = True
673 self.dsa_is_ro = False
675 if "msDS-Behavior-Version" in msg:
676 self.dsa_behavior = int(msg['msDS-Behavior-Version'][0])
678 # Load the NC replicas that are enumerated on this dsa
679 self.load_current_replica_table(samdb)
681 # Load the nTDSConnection that are enumerated on this dsa
682 self.load_connection_table(samdb)
684 def load_current_replica_table(self, samdb):
685 """Method to load the NC replica's listed for DSA object.
687 This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs,
688 hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, and
689 msDS-HasInstantiatedNCs) to determine complete list of NC replicas that
690 are enumerated for the DSA. Once a NC replica is loaded it is
691 identified (schema, config, etc) and the other replica attributes
692 (partial, ro, etc) are determined.
694 :param samdb: database to query for DSA replica list
697 # not RODC - default, config, schema (old style)
699 # not RODC - default, config, schema, app NCs
701 # domain NC partial replicas
702 "hasPartialReplicaNCs",
705 # RODC only - default, config, schema, app NCs
706 "msDS-hasFullReplicaNCs",
707 # Identifies if replica is coming, going, or stable
708 "msDS-HasInstantiatedNCs"
711 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
714 except ldb.LdbError as e6:
715 (enum, estr) = e6.args
716 raise KCCError("Unable to find nTDSDSA NCs for (%s) - (%s)" %
717 (self.dsa_dnstr, estr))
719 # The table of NCs for the dsa we are searching
722 # We should get one response to our query here for
723 # the ntds that we requested
726 # Our response will contain a number of elements including
727 # the dn of the dsa as well as elements for each
728 # attribute (e.g. hasMasterNCs). Each of these elements
729 # is a dictonary list which we retrieve the keys for and
730 # then iterate over them
731 for k in res[0].keys():
735 # For each attribute type there will be one or more DNs
736 # listed. For instance DCs normally have 3 hasMasterNCs
738 for value in res[0][k]:
739 # Turn dn into a dsdb_Dn so we can use
740 # its methods to parse a binary DN
741 dsdn = dsdb_Dn(samdb, value)
742 flags = dsdn.get_binary_integer()
745 if not dnstr in tmp_table:
746 rep = NCReplica(self, dnstr)
747 tmp_table[dnstr] = rep
749 rep = tmp_table[dnstr]
751 if k == "msDS-HasInstantiatedNCs":
752 rep.set_instantiated_flags(flags)
755 rep.identify_by_dsa_attr(samdb, k)
757 # if we've identified the default domain NC
758 # then save its DN string
760 self.default_dnstr = dnstr
762 raise KCCError("No nTDSDSA NCs for (%s)" % self.dsa_dnstr)
764 # Assign our newly built NC replica table to this dsa
765 self.current_rep_table = tmp_table
767 def add_needed_replica(self, rep):
768 """Method to add a NC replica that "should be present" to the
771 self.needed_rep_table[rep.nc_dnstr] = rep
773 def load_connection_table(self, samdb):
774 """Method to load the nTDSConnections listed for DSA object.
776 :param samdb: database to query for DSA connection list
779 res = samdb.search(base=self.dsa_dnstr,
780 scope=ldb.SCOPE_SUBTREE,
781 expression="(objectClass=nTDSConnection)")
783 except ldb.LdbError as e7:
784 (enum, estr) = e7.args
785 raise KCCError("Unable to find nTDSConnection for (%s) - (%s)" %
786 (self.dsa_dnstr, estr))
792 if dnstr in self.connect_table:
795 connect = NTDSConnection(dnstr)
797 connect.load_connection(samdb)
798 self.connect_table[dnstr] = connect
800 def commit_connections(self, samdb, ro=False):
801 """Method to commit any uncommitted nTDSConnections
802 modifications that are in our table. These would be
803 identified connections that are marked to be added or
806 :param samdb: database to commit DSA connection list to
807 :param ro: if (true) then peform internal operations but
808 do not write to the database (readonly)
812 for dnstr, connect in self.connect_table.items():
813 if connect.to_be_added:
814 connect.commit_added(samdb, ro)
816 if connect.to_be_modified:
817 connect.commit_modified(samdb, ro)
819 if connect.to_be_deleted:
820 connect.commit_deleted(samdb, ro)
821 delconn.append(dnstr)
823 # Now delete the connection from the table
824 for dnstr in delconn:
825 del self.connect_table[dnstr]
827 def add_connection(self, dnstr, connect):
828 assert dnstr not in self.connect_table
829 self.connect_table[dnstr] = connect
831 def get_connection_by_from_dnstr(self, from_dnstr):
832 """Scan DSA nTDSConnection table and return connection
833 with a "fromServer" dn string equivalent to method
836 :param from_dnstr: search for this from server entry
839 for connect in self.connect_table.values():
840 if connect.get_from_dnstr() == from_dnstr:
841 answer.append(connect)
845 def dumpstr_current_replica_table(self):
846 '''Debug dump string output of current replica table'''
847 return '\n'.join(str(x) for x in self.current_rep_table)
849 def dumpstr_needed_replica_table(self):
850 '''Debug dump string output of needed replica table'''
851 return '\n'.join(str(x) for x in self.needed_rep_table)
853 def dumpstr_connect_table(self):
854 '''Debug dump string output of connect table'''
855 return '\n'.join(str(x) for x in self.connect_table)
857 def new_connection(self, options, system_flags, transport, from_dnstr,
859 """Set up a new connection for the DSA based on input
860 parameters. Connection will be added to the DSA
861 connect_table and will be marked as "to be added" pending
862 a call to commit_connections()
864 dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
866 connect = NTDSConnection(dnstr)
867 connect.to_be_added = True
868 connect.enabled = True
869 connect.from_dnstr = from_dnstr
870 connect.options = options
871 connect.system_flags = system_flags
873 if transport is not None:
874 connect.transport_dnstr = transport.dnstr
875 connect.transport_guid = transport.guid
877 if sched is not None:
878 connect.schedule = sched
880 # Create schedule. Attribute valuse set according to MS-TECH
881 # intrasite connection creation document
882 connect.schedule = new_connection_schedule()
884 self.add_connection(dnstr, connect)
888 class NTDSConnection(object):
889 """Class defines a nTDSConnection found under a DSA
891 def __init__(self, dnstr):
896 self.to_be_added = False # new connection needs to be added
897 self.to_be_deleted = False # old connection needs to be deleted
898 self.to_be_modified = False
900 self.system_flags = 0
901 self.transport_dnstr = None
902 self.transport_guid = None
903 self.from_dnstr = None
907 '''Debug dump string output of NTDSConnection object'''
909 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
910 text = text + "\n\tenabled=%s" % self.enabled
911 text = text + "\n\tto_be_added=%s" % self.to_be_added
912 text = text + "\n\tto_be_deleted=%s" % self.to_be_deleted
913 text = text + "\n\tto_be_modified=%s" % self.to_be_modified
914 text = text + "\n\toptions=0x%08X" % self.options
915 text = text + "\n\tsystem_flags=0x%08X" % self.system_flags
916 text = text + "\n\twhenCreated=%d" % self.whenCreated
917 text = text + "\n\ttransport_dn=%s" % self.transport_dnstr
919 if self.guid is not None:
920 text = text + "\n\tguid=%s" % str(self.guid)
922 if self.transport_guid is not None:
923 text = text + "\n\ttransport_guid=%s" % str(self.transport_guid)
925 text = text + "\n\tfrom_dn=%s" % self.from_dnstr
927 if self.schedule is not None:
928 text += "\n\tschedule.size=%s" % self.schedule.size
929 text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
930 text += ("\n\tschedule.numberOfSchedules=%s" %
931 self.schedule.numberOfSchedules)
933 for i, header in enumerate(self.schedule.headerArray):
934 text += ("\n\tschedule.headerArray[%d].type=%d" %
936 text += ("\n\tschedule.headerArray[%d].offset=%d" %
938 text += "\n\tschedule.dataArray[%d].slots[ " % i
939 for slot in self.schedule.dataArray[i].slots:
940 text = text + "0x%X " % slot
945 def load_connection(self, samdb):
946 """Given a NTDSConnection object with an prior initialization
947 for the object's DN, search for the DN and load attributes
959 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
962 except ldb.LdbError as e8:
963 (enum, estr) = e8.args
964 raise KCCError("Unable to find nTDSConnection for (%s) - (%s)" %
970 self.options = int(msg["options"][0])
972 if "enabledConnection" in msg:
973 if msg["enabledConnection"][0].upper().lstrip().rstrip() == "TRUE":
976 if "systemFlags" in msg:
977 self.system_flags = int(msg["systemFlags"][0])
981 misc.GUID(samdb.schema_format_value("objectGUID",
982 msg["objectGUID"][0]))
984 raise KCCError("Unable to find objectGUID in nTDSConnection "
985 "for (%s)" % (self.dnstr))
987 if "transportType" in msg:
988 dsdn = dsdb_Dn(samdb, msg["transportType"][0])
989 self.load_connection_transport(samdb, str(dsdn.dn))
991 if "schedule" in msg:
992 self.schedule = ndr_unpack(drsblobs.schedule, msg["schedule"][0])
994 if "whenCreated" in msg:
995 self.whenCreated = ldb.string_to_time(msg["whenCreated"][0])
997 if "fromServer" in msg:
998 dsdn = dsdb_Dn(samdb, msg["fromServer"][0])
999 self.from_dnstr = str(dsdn.dn)
1000 assert self.from_dnstr is not None
1002 def load_connection_transport(self, samdb, tdnstr):
1003 """Given a NTDSConnection object which enumerates a transport
1004 DN, load the transport information for the connection object
1006 :param tdnstr: transport DN to load
1008 attrs = ["objectGUID"]
1010 res = samdb.search(base=tdnstr,
1011 scope=ldb.SCOPE_BASE, attrs=attrs)
1013 except ldb.LdbError as e9:
1014 (enum, estr) = e9.args
1015 raise KCCError("Unable to find transport (%s) - (%s)" %
1018 if "objectGUID" in res[0]:
1020 self.transport_dnstr = tdnstr
1021 self.transport_guid = \
1022 misc.GUID(samdb.schema_format_value("objectGUID",
1023 msg["objectGUID"][0]))
1024 assert self.transport_dnstr is not None
1025 assert self.transport_guid is not None
1027 def commit_deleted(self, samdb, ro=False):
1028 """Local helper routine for commit_connections() which
1029 handles committed connections that are to be deleted from
1030 the database database
1032 assert self.to_be_deleted
1033 self.to_be_deleted = False
1035 # No database modification requested
1040 samdb.delete(self.dnstr)
1041 except ldb.LdbError as e10:
1042 (enum, estr) = e10.args
1043 raise KCCError("Could not delete nTDSConnection for (%s) - (%s)" %
1046 def commit_added(self, samdb, ro=False):
1047 """Local helper routine for commit_connections() which
1048 handles committed connections that are to be added to the
1051 assert self.to_be_added
1052 self.to_be_added = False
1054 # No database modification requested
1058 # First verify we don't have this entry to ensure nothing
1059 # is programatically amiss
1062 msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1066 except ldb.LdbError as e11:
1067 (enum, estr) = e11.args
1068 if enum != ldb.ERR_NO_SUCH_OBJECT:
1069 raise KCCError("Unable to search for (%s) - (%s)" %
1072 raise KCCError("nTDSConnection for (%s) already exists!" %
1080 # Prepare a message for adding to the samdb
1082 m.dn = ldb.Dn(samdb, self.dnstr)
1084 m["objectClass"] = \
1085 ldb.MessageElement("nTDSConnection", ldb.FLAG_MOD_ADD,
1087 m["showInAdvancedViewOnly"] = \
1088 ldb.MessageElement("TRUE", ldb.FLAG_MOD_ADD,
1089 "showInAdvancedViewOnly")
1090 m["enabledConnection"] = \
1091 ldb.MessageElement(enablestr, ldb.FLAG_MOD_ADD,
1092 "enabledConnection")
1094 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_ADD, "fromServer")
1096 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options")
1097 m["systemFlags"] = \
1098 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_ADD,
1101 if self.transport_dnstr is not None:
1102 m["transportType"] = \
1103 ldb.MessageElement(str(self.transport_dnstr), ldb.FLAG_MOD_ADD,
1106 if self.schedule is not None:
1108 ldb.MessageElement(ndr_pack(self.schedule),
1109 ldb.FLAG_MOD_ADD, "schedule")
1112 except ldb.LdbError as e12:
1113 (enum, estr) = e12.args
1114 raise KCCError("Could not add nTDSConnection for (%s) - (%s)" %
1117 def commit_modified(self, samdb, ro=False):
1118 """Local helper routine for commit_connections() which
1119 handles committed connections that are to be modified to the
1122 assert self.to_be_modified
1123 self.to_be_modified = False
1125 # No database modification requested
1129 # First verify we have this entry to ensure nothing
1130 # is programatically amiss
1132 # we don't use the search result, but it tests the status
1133 # of self.dnstr in the database.
1134 samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1136 except ldb.LdbError as e13:
1137 (enum, estr) = e13.args
1138 if enum == ldb.ERR_NO_SUCH_OBJECT:
1139 raise KCCError("nTDSConnection for (%s) doesn't exist!" %
1141 raise KCCError("Unable to search for (%s) - (%s)" %
1149 # Prepare a message for modifying the samdb
1151 m.dn = ldb.Dn(samdb, self.dnstr)
1153 m["enabledConnection"] = \
1154 ldb.MessageElement(enablestr, ldb.FLAG_MOD_REPLACE,
1155 "enabledConnection")
1157 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_REPLACE,
1160 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_REPLACE,
1162 m["systemFlags"] = \
1163 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_REPLACE,
1166 if self.transport_dnstr is not None:
1167 m["transportType"] = \
1168 ldb.MessageElement(str(self.transport_dnstr),
1169 ldb.FLAG_MOD_REPLACE, "transportType")
1171 m["transportType"] = \
1172 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "transportType")
1174 if self.schedule is not None:
1176 ldb.MessageElement(ndr_pack(self.schedule),
1177 ldb.FLAG_MOD_REPLACE, "schedule")
1180 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "schedule")
1183 except ldb.LdbError as e14:
1184 (enum, estr) = e14.args
1185 raise KCCError("Could not modify nTDSConnection for (%s) - (%s)" %
1188 def set_modified(self, truefalse):
1189 self.to_be_modified = truefalse
1191 def is_schedule_minimum_once_per_week(self):
1192 """Returns True if our schedule includes at least one
1193 replication interval within the week. False otherwise
1195 # replinfo schedule is None means "always", while
1196 # NTDSConnection schedule is None means "never".
1197 if self.schedule is None or self.schedule.dataArray[0] is None:
1200 for slot in self.schedule.dataArray[0].slots:
1201 if (slot & 0x0F) != 0x0:
1205 def is_equivalent_schedule(self, sched):
1206 """Returns True if our schedule is equivalent to the input
1207 comparison schedule.
1209 :param shed: schedule to compare to
1211 # There are 4 cases, where either self.schedule or sched can be None
1213 # | self. is None | self. is not None
1214 # --------------+-----------------+--------------------
1215 # sched is None | True | False
1216 # --------------+-----------------+--------------------
1217 # sched is not None | False | do calculations
1219 if self.schedule is None:
1220 return sched is None
1225 if ((self.schedule.size != sched.size or
1226 self.schedule.bandwidth != sched.bandwidth or
1227 self.schedule.numberOfSchedules != sched.numberOfSchedules)):
1230 for i, header in enumerate(self.schedule.headerArray):
1232 if self.schedule.headerArray[i].type != sched.headerArray[i].type:
1235 if self.schedule.headerArray[i].offset != \
1236 sched.headerArray[i].offset:
1239 for a, b in zip(self.schedule.dataArray[i].slots,
1240 sched.dataArray[i].slots):
1245 def is_rodc_topology(self):
1246 """Returns True if NTDS Connection specifies RODC
1249 if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
1253 def is_generated(self):
1254 """Returns True if NTDS Connection was generated by the
1255 KCC topology algorithm as opposed to set by the administrator
1257 if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0:
1261 def is_override_notify_default(self):
1262 """Returns True if NTDS Connection should override notify default
1264 if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0:
1268 def is_use_notify(self):
1269 """Returns True if NTDS Connection should use notify
1271 if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0:
1275 def is_twoway_sync(self):
1276 """Returns True if NTDS Connection should use twoway sync
1278 if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0:
1282 def is_intersite_compression_disabled(self):
1283 """Returns True if NTDS Connection intersite compression
1286 if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0:
1290 def is_user_owned_schedule(self):
1291 """Returns True if NTDS Connection has a user owned schedule
1293 if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0:
1297 def is_enabled(self):
1298 """Returns True if NTDS Connection is enabled
1302 def get_from_dnstr(self):
1303 '''Return fromServer dn string attribute'''
1304 return self.from_dnstr
1307 class Partition(NamingContext):
1308 """A naming context discovered thru Partitions DN of the config schema.
1310 This is a more specific form of NamingContext class (inheriting from that
1311 class) and it identifies unique attributes enumerated in the Partitions
1312 such as which nTDSDSAs are cross referenced for replicas
1314 def __init__(self, partstr):
1315 self.partstr = partstr
1317 self.system_flags = 0
1318 self.rw_location_list = []
1319 self.ro_location_list = []
1321 # We don't have enough info to properly
1322 # fill in the naming context yet. We'll get that
1323 # fully set up with load_partition().
1324 NamingContext.__init__(self, None)
1326 def load_partition(self, samdb):
1327 """Given a Partition class object that has been initialized with its
1328 partition dn string, load the partition from the sam database, identify
1329 the type of the partition (schema, domain, etc) and record the list of
1330 nTDSDSAs that appear in the cross reference attributes
1331 msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations.
1333 :param samdb: sam database to load partition from
1338 "msDS-NC-Replica-Locations",
1339 "msDS-NC-RO-Replica-Locations"]
1341 res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE,
1344 except ldb.LdbError as e15:
1345 (enum, estr) = e15.args
1346 raise KCCError("Unable to find partition for (%s) - (%s)" %
1347 (self.partstr, estr))
1349 for k in msg.keys():
1354 if msg[k][0].upper().lstrip().rstrip() == "TRUE":
1357 self.enabled = False
1360 if k == "systemFlags":
1361 self.system_flags = int(msg[k][0])
1364 for value in msg[k]:
1365 dsdn = dsdb_Dn(samdb, value)
1366 dnstr = str(dsdn.dn)
1369 self.nc_dnstr = dnstr
1372 if k == "msDS-NC-Replica-Locations":
1373 self.rw_location_list.append(dnstr)
1376 if k == "msDS-NC-RO-Replica-Locations":
1377 self.ro_location_list.append(dnstr)
1380 # Now identify what type of NC this partition
1382 self.identify_by_basedn(samdb)
1384 def is_enabled(self):
1385 """Returns True if partition is enabled
1387 return self.is_enabled
1389 def is_foreign(self):
1390 """Returns True if this is not an Active Directory NC in our
1391 forest but is instead something else (e.g. a foreign NC)
1393 if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0:
1398 def should_be_present(self, target_dsa):
1399 """Tests whether this partition should have an NC replica
1400 on the target dsa. This method returns a tuple of
1401 needed=True/False, ro=True/False, partial=True/False
1403 :param target_dsa: should NC be present on target dsa
1408 # If this is the config, schema, or default
1409 # domain NC for the target dsa then it should
1411 needed = (self.nc_type == NCType.config or
1412 self.nc_type == NCType.schema or
1413 (self.nc_type == NCType.domain and
1414 self.nc_dnstr == target_dsa.default_dnstr))
1416 # A writable replica of an application NC should be present
1417 # if there a cross reference to the target DSA exists. Depending
1418 # on whether the DSA is ro we examine which type of cross reference
1419 # to look for (msDS-NC-Replica-Locations or
1420 # msDS-NC-RO-Replica-Locations
1421 if self.nc_type == NCType.application:
1422 if target_dsa.is_ro():
1423 if target_dsa.dsa_dnstr in self.ro_location_list:
1426 if target_dsa.dsa_dnstr in self.rw_location_list:
1429 # If the target dsa is a gc then a partial replica of a
1430 # domain NC (other than the DSAs default domain) should exist
1431 # if there is also a cross reference for the DSA
1432 if (target_dsa.is_gc() and
1433 self.nc_type == NCType.domain and
1434 self.nc_dnstr != target_dsa.default_dnstr and
1435 (target_dsa.dsa_dnstr in self.ro_location_list or
1436 target_dsa.dsa_dnstr in self.rw_location_list)):
1440 # partial NCs are always readonly
1441 if needed and (target_dsa.is_ro() or partial):
1444 return needed, ro, partial
1447 '''Debug dump string output of class'''
1448 text = "%s" % NamingContext.__str__(self)
1449 text = text + "\n\tpartdn=%s" % self.partstr
1450 for k in self.rw_location_list:
1451 text = text + "\n\tmsDS-NC-Replica-Locations=%s" % k
1452 for k in self.ro_location_list:
1453 text = text + "\n\tmsDS-NC-RO-Replica-Locations=%s" % k
1458 """An individual site object discovered thru the configuration
1459 naming context. Contains all DSAs that exist within the site
1461 def __init__(self, site_dnstr, nt_now):
1462 self.site_dnstr = site_dnstr
1463 self.site_guid = None
1464 self.site_options = 0
1465 self.site_topo_generator = None
1466 self.site_topo_failover = 0 # appears to be in minutes
1468 self.rw_dsa_table = {}
1469 self.nt_now = nt_now
1471 def load_site(self, samdb):
1472 """Loads the NTDS Site Settings options attribute for the site
1473 as well as querying and loading all DSAs that appear within
1476 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1478 "interSiteTopologyFailover",
1479 "interSiteTopologyGenerator"]
1481 res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE,
1483 self_res = samdb.search(base=self.site_dnstr, scope=ldb.SCOPE_BASE,
1484 attrs=['objectGUID'])
1485 except ldb.LdbError as e16:
1486 (enum, estr) = e16.args
1487 raise KCCError("Unable to find site settings for (%s) - (%s)" %
1491 if "options" in msg:
1492 self.site_options = int(msg["options"][0])
1494 if "interSiteTopologyGenerator" in msg:
1495 self.site_topo_generator = \
1496 str(msg["interSiteTopologyGenerator"][0])
1498 if "interSiteTopologyFailover" in msg:
1499 self.site_topo_failover = int(msg["interSiteTopologyFailover"][0])
1502 if "objectGUID" in msg:
1503 self.site_guid = misc.GUID(samdb.schema_format_value("objectGUID",
1504 msg["objectGUID"][0]))
1506 self.load_all_dsa(samdb)
1508 def load_all_dsa(self, samdb):
1509 """Discover all nTDSDSA thru the sites entry and
1510 instantiate and load the DSAs. Each dsa is inserted
1511 into the dsa_table by dn string.
1514 res = samdb.search(self.site_dnstr,
1515 scope=ldb.SCOPE_SUBTREE,
1516 expression="(objectClass=nTDSDSA)")
1517 except ldb.LdbError as e17:
1518 (enum, estr) = e17.args
1519 raise KCCError("Unable to find nTDSDSAs - (%s)" % estr)
1525 if dnstr in self.dsa_table:
1528 dsa = DirectoryServiceAgent(dnstr)
1532 # Assign this dsa to my dsa table
1533 # and index by dsa dn
1534 self.dsa_table[dnstr] = dsa
1536 self.rw_dsa_table[dnstr] = dsa
1538 def get_dsa(self, dnstr):
1539 """Return a previously loaded DSA object by consulting
1540 the sites dsa_table for the provided DSA dn string
1542 :return: None if DSA doesn't exist
1544 return self.dsa_table.get(dnstr)
1546 def select_istg(self, samdb, mydsa, ro):
1547 """Determine if my DC should be an intersite topology
1548 generator. If my DC is the istg and is both a writeable
1549 DC and the database is opened in write mode then we perform
1550 an originating update to set the interSiteTopologyGenerator
1551 attribute in the NTDS Site Settings object. An RODC always
1552 acts as an ISTG for itself.
1554 # The KCC on an RODC always acts as an ISTG for itself
1556 mydsa.dsa_is_istg = True
1557 self.site_topo_generator = mydsa.dsa_dnstr
1560 c_rep = get_dsa_config_rep(mydsa)
1562 # Load repsFrom and replUpToDateVector if not already loaded
1563 # so we can get the current state of the config replica and
1564 # whether we are getting updates from the istg
1565 c_rep.load_repsFrom(samdb)
1567 c_rep.load_replUpToDateVector(samdb)
1569 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1570 # First, the KCC on a writable DC determines whether it acts
1571 # as an ISTG for its site
1573 # Let s be the object such that s!lDAPDisplayName = nTDSDSA
1574 # and classSchema in s!objectClass.
1576 # Let D be the sequence of objects o in the site of the local
1577 # DC such that o!objectCategory = s. D is sorted in ascending
1578 # order by objectGUID.
1580 # Which is a fancy way of saying "sort all the nTDSDSA objects
1581 # in the site by guid in ascending order". Place sorted list
1583 D_sort = sorted(self.rw_dsa_table.values(), cmp=sort_dsa_by_guid)
1585 # double word number of 100 nanosecond intervals since 1600s
1587 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1588 # if o!interSiteTopologyFailover is 0 or has no value.
1590 # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1591 # so it appears we have to turn f into the same interval
1593 # interSiteTopologyFailover (if set) appears to be in minutes
1594 # so we'll need to convert to senconds and then 100 nanosecond
1596 # XXX [MS-ADTS] 6.2.2.3.1 says it is seconds, not minutes.
1598 # 10,000,000 is number of 100 nanosecond intervals in a second
1599 if self.site_topo_failover == 0:
1600 f = 2 * 60 * 60 * 10000000
1602 f = self.site_topo_failover * 60 * 10000000
1604 # Let o be the site settings object for the site of the local
1605 # DC, or NULL if no such o exists.
1606 d_dsa = self.dsa_table.get(self.site_topo_generator)
1608 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1609 # If o != NULL and o!interSiteTopologyGenerator is not the
1610 # nTDSDSA object for the local DC and
1611 # o!interSiteTopologyGenerator is an element dj of sequence D:
1613 if d_dsa is not None and d_dsa is not mydsa:
1614 # From MS-ADTS 6.2.2.3.1 ISTG Selection:
1615 # Let c be the cursor in the replUpToDateVector variable
1616 # associated with the NC replica of the config NC such
1617 # that c.uuidDsa = dj!invocationId. If no such c exists
1618 # (No evidence of replication from current ITSG):
1622 # Else if the current time < c.timeLastSyncSuccess - f
1623 # (Evidence of time sync problem on current ISTG):
1627 # Else (Evidence of replication from current ITSG):
1629 # Let t = c.timeLastSyncSuccess.
1631 # last_success appears to be a double word containing
1632 # number of 100 nanosecond intervals since the 1600s
1633 j_idx = D_sort.index(d_dsa)
1636 for cursor in c_rep.rep_replUpToDateVector_cursors:
1637 if d_dsa.dsa_ivid == cursor.source_dsa_invocation_id:
1645 #XXX doc says current time < c.timeLastSyncSuccess - f
1646 # which is true only if f is negative or clocks are wrong.
1647 # f is not negative in the default case (2 hours).
1648 elif self.nt_now - cursor.last_sync_success > f:
1653 t_time = cursor.last_sync_success
1655 # Otherwise (Nominate local DC as ISTG):
1656 # Let i be the integer such that di is the nTDSDSA
1657 # object for the local DC.
1658 # Let t = the current time.
1660 i_idx = D_sort.index(mydsa)
1661 t_time = self.nt_now
1663 # Compute a function that maintains the current ISTG if
1664 # it is alive, cycles through other candidates if not.
1666 # Let k be the integer (i + ((current time - t) /
1667 # o!interSiteTopologyFailover)) MOD |D|.
1669 # Note: We don't want to divide by zero here so they must
1670 # have meant "f" instead of "o!interSiteTopologyFailover"
1671 k_idx = (i_idx + ((self.nt_now - t_time) / f)) % len(D_sort)
1673 # The local writable DC acts as an ISTG for its site if and
1674 # only if dk is the nTDSDSA object for the local DC. If the
1675 # local DC does not act as an ISTG, the KCC skips the
1676 # remainder of this task.
1677 d_dsa = D_sort[k_idx]
1678 d_dsa.dsa_is_istg = True
1680 # Update if we are the ISTG, otherwise return
1681 if d_dsa is not mydsa:
1685 if self.site_topo_generator == mydsa.dsa_dnstr:
1688 self.site_topo_generator = mydsa.dsa_dnstr
1690 # If readonly database then do not perform a
1695 # Perform update to the samdb
1696 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1699 m.dn = ldb.Dn(samdb, ssdn)
1701 m["interSiteTopologyGenerator"] = \
1702 ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
1703 "interSiteTopologyGenerator")
1707 except ldb.LdbError as estr:
1709 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1713 def is_intrasite_topology_disabled(self):
1714 '''Returns True if intra-site topology is disabled for site'''
1715 return (self.site_options &
1716 dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0
1718 def is_intersite_topology_disabled(self):
1719 '''Returns True if inter-site topology is disabled for site'''
1720 return ((self.site_options &
1721 dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED)
1724 def is_random_bridgehead_disabled(self):
1725 '''Returns True if selection of random bridgehead is disabled'''
1726 return (self.site_options &
1727 dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0
1729 def is_detect_stale_disabled(self):
1730 '''Returns True if detect stale is disabled for site'''
1731 return (self.site_options &
1732 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0
1734 def is_cleanup_ntdsconn_disabled(self):
1735 '''Returns True if NTDS Connection cleanup is disabled for site'''
1736 return (self.site_options &
1737 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0
1739 def same_site(self, dsa):
1740 '''Return True if dsa is in this site'''
1741 if self.get_dsa(dsa.dsa_dnstr):
1745 def is_rodc_site(self):
1746 if len(self.dsa_table) > 0 and len(self.rw_dsa_table) == 0:
1751 '''Debug dump string output of class'''
1752 text = "%s:" % self.__class__.__name__
1753 text = text + "\n\tdn=%s" % self.site_dnstr
1754 text = text + "\n\toptions=0x%X" % self.site_options
1755 text = text + "\n\ttopo_generator=%s" % self.site_topo_generator
1756 text = text + "\n\ttopo_failover=%d" % self.site_topo_failover
1757 for key, dsa in self.dsa_table.items():
1758 text = text + "\n%s" % dsa
1762 class GraphNode(object):
1763 """A graph node describing a set of edges that should be directed to it.
1765 Each edge is a connection for a particular naming context replica directed
1766 from another node in the forest to this node.
1769 def __init__(self, dsa_dnstr, max_node_edges):
1770 """Instantiate the graph node according to a DSA dn string
1772 :param max_node_edges: maximum number of edges that should ever
1773 be directed to the node
1775 self.max_edges = max_node_edges
1776 self.dsa_dnstr = dsa_dnstr
1780 text = "%s:" % self.__class__.__name__
1781 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
1782 text = text + "\n\tmax_edges=%d" % self.max_edges
1784 for i, edge in enumerate(self.edge_from):
1785 if isinstance(edge, str):
1786 text += "\n\tedge_from[%d]=%s" % (i, edge)
1790 def add_edge_from(self, from_dsa_dnstr):
1791 """Add an edge from the dsa to our graph nodes edge from list
1793 :param from_dsa_dnstr: the dsa that the edge emanates from
1795 assert isinstance(from_dsa_dnstr, str)
1797 # No edges from myself to myself
1798 if from_dsa_dnstr == self.dsa_dnstr:
1800 # Only one edge from a particular node
1801 if from_dsa_dnstr in self.edge_from:
1803 # Not too many edges
1804 if len(self.edge_from) >= self.max_edges:
1806 self.edge_from.append(from_dsa_dnstr)
1809 def add_edges_from_connections(self, dsa):
1810 """For each nTDSConnection object associated with a particular
1811 DSA, we test if it implies an edge to this graph node (i.e.
1812 the "fromServer" attribute). If it does then we add an
1813 edge from the server unless we are over the max edges for this
1816 :param dsa: dsa with a dnstr equivalent to his graph node
1818 for connect in dsa.connect_table.values():
1819 self.add_edge_from(connect.from_dnstr)
1821 def add_connections_from_edges(self, dsa, transport):
1822 """For each edge directed to this graph node, ensure there
1823 is a corresponding nTDSConnection object in the dsa.
1825 for edge_dnstr in self.edge_from:
1826 connections = dsa.get_connection_by_from_dnstr(edge_dnstr)
1828 # For each edge directed to the NC replica that
1829 # "should be present" on the local DC, the KCC determines
1830 # whether an object c exists such that:
1832 # c is a child of the DC's nTDSDSA object.
1833 # c.objectCategory = nTDSConnection
1835 # Given the NC replica ri from which the edge is directed,
1836 # c.fromServer is the dsname of the nTDSDSA object of
1837 # the DC on which ri "is present".
1839 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1842 for connect in connections:
1843 if connect.is_rodc_topology():
1850 # if no such object exists then the KCC adds an object
1851 # c with the following attributes
1853 # Generate a new dnstr for this nTDSConnection
1854 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1855 flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1856 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1858 dsa.new_connection(opt, flags, transport, edge_dnstr, None)
1860 def has_sufficient_edges(self):
1861 '''Return True if we have met the maximum "from edges" criteria'''
1862 if len(self.edge_from) >= self.max_edges:
1867 class Transport(object):
1868 """Class defines a Inter-site transport found under Sites
1871 def __init__(self, dnstr):
1876 self.address_attr = None
1877 self.bridgehead_list = []
1880 '''Debug dump string output of Transport object'''
1882 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
1883 text = text + "\n\tguid=%s" % str(self.guid)
1884 text = text + "\n\toptions=%d" % self.options
1885 text = text + "\n\taddress_attr=%s" % self.address_attr
1886 text = text + "\n\tname=%s" % self.name
1887 for dnstr in self.bridgehead_list:
1888 text = text + "\n\tbridgehead_list=%s" % dnstr
1892 def load_transport(self, samdb):
1893 """Given a Transport object with an prior initialization
1894 for the object's DN, search for the DN and load attributes
1897 attrs = ["objectGUID",
1900 "bridgeheadServerListBL",
1901 "transportAddressAttribute"]
1903 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
1906 except ldb.LdbError as e18:
1907 (enum, estr) = e18.args
1908 raise KCCError("Unable to find Transport for (%s) - (%s)" %
1912 self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
1913 msg["objectGUID"][0]))
1915 if "options" in msg:
1916 self.options = int(msg["options"][0])
1918 if "transportAddressAttribute" in msg:
1919 self.address_attr = str(msg["transportAddressAttribute"][0])
1922 self.name = str(msg["name"][0])
1924 if "bridgeheadServerListBL" in msg:
1925 for value in msg["bridgeheadServerListBL"]:
1926 dsdn = dsdb_Dn(samdb, value)
1927 dnstr = str(dsdn.dn)
1928 if dnstr not in self.bridgehead_list:
1929 self.bridgehead_list.append(dnstr)
1932 class RepsFromTo(object):
1933 """Class encapsulation of the NDR repsFromToBlob.
1935 Removes the necessity of external code having to
1936 understand about other_info or manipulation of
1939 def __init__(self, nc_dnstr=None, ndr_blob=None):
1941 self.__dict__['to_be_deleted'] = False
1942 self.__dict__['nc_dnstr'] = nc_dnstr
1943 self.__dict__['update_flags'] = 0x0
1944 # XXX the following sounds dubious and/or better solved
1945 # elsewhere, but lets leave it for now. In particular, there
1946 # seems to be no reason for all the non-ndr generated
1947 # attributes to be handled in the round about way (e.g.
1948 # self.__dict__['to_be_deleted'] = False above). On the other
1949 # hand, it all seems to work. Hooray! Hands off!.
1953 # There is a very subtle bug here with python
1954 # and our NDR code. If you assign directly to
1955 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1956 # then a proper python GC reference count is not
1959 # To work around this we maintain an internal
1960 # reference to "dns_name(x)" and "other_info" elements
1961 # of repsFromToBlob. This internal reference
1962 # is hidden within this class but it is why you
1963 # see statements like this below:
1965 # self.__dict__['ndr_blob'].ctr.other_info = \
1966 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1968 # That would appear to be a redundant assignment but
1969 # it is necessary to hold a proper python GC reference
1971 if ndr_blob is None:
1972 self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
1973 self.__dict__['ndr_blob'].version = 0x1
1974 self.__dict__['dns_name1'] = None
1975 self.__dict__['dns_name2'] = None
1977 self.__dict__['ndr_blob'].ctr.other_info = \
1978 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1981 self.__dict__['ndr_blob'] = ndr_blob
1982 self.__dict__['other_info'] = ndr_blob.ctr.other_info
1984 if ndr_blob.version == 0x1:
1985 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
1986 self.__dict__['dns_name2'] = None
1988 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
1989 self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
1992 '''Debug dump string output of class'''
1994 text = "%s:" % self.__class__.__name__
1995 text += "\n\tdnstr=%s" % self.nc_dnstr
1996 text += "\n\tupdate_flags=0x%X" % self.update_flags
1997 text += "\n\tversion=%d" % self.version
1998 text += "\n\tsource_dsa_obj_guid=%s" % self.source_dsa_obj_guid
1999 text += ("\n\tsource_dsa_invocation_id=%s" %
2000 self.source_dsa_invocation_id)
2001 text += "\n\ttransport_guid=%s" % self.transport_guid
2002 text += "\n\treplica_flags=0x%X" % self.replica_flags
2003 text += ("\n\tconsecutive_sync_failures=%d" %
2004 self.consecutive_sync_failures)
2005 text += "\n\tlast_success=%s" % self.last_success
2006 text += "\n\tlast_attempt=%s" % self.last_attempt
2007 text += "\n\tdns_name1=%s" % self.dns_name1
2008 text += "\n\tdns_name2=%s" % self.dns_name2
2009 text += "\n\tschedule[ "
2010 for slot in self.schedule:
2011 text += "0x%X " % slot
2016 def __setattr__(self, item, value):
2017 """Set an attribute and chyange update flag.
2019 Be aware that setting any RepsFromTo attribute will set the
2020 drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS update flag.
2022 if item in ['schedule', 'replica_flags', 'transport_guid',
2023 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2024 'consecutive_sync_failures', 'last_success',
2027 if item in ['replica_flags']:
2028 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
2029 elif item in ['schedule']:
2030 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
2032 setattr(self.__dict__['ndr_blob'].ctr, item, value)
2034 elif item in ['dns_name1']:
2035 self.__dict__['dns_name1'] = value
2037 if self.__dict__['ndr_blob'].version == 0x1:
2038 self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
2039 self.__dict__['dns_name1']
2041 self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
2042 self.__dict__['dns_name1']
2044 elif item in ['dns_name2']:
2045 self.__dict__['dns_name2'] = value
2047 if self.__dict__['ndr_blob'].version == 0x1:
2048 raise AttributeError(item)
2050 self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
2051 self.__dict__['dns_name2']
2053 elif item in ['nc_dnstr']:
2054 self.__dict__['nc_dnstr'] = value
2056 elif item in ['to_be_deleted']:
2057 self.__dict__['to_be_deleted'] = value
2059 elif item in ['version']:
2060 raise AttributeError("Attempt to set readonly attribute %s" % item)
2062 raise AttributeError("Unknown attribute %s" % item)
2064 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
2066 def __getattr__(self, item):
2067 """Overload of RepsFromTo attribute retrieval.
2069 Allows external code to ignore substructures within the blob
2071 if item in ['schedule', 'replica_flags', 'transport_guid',
2072 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2073 'consecutive_sync_failures', 'last_success',
2075 return getattr(self.__dict__['ndr_blob'].ctr, item)
2077 elif item in ['version']:
2078 return self.__dict__['ndr_blob'].version
2080 elif item in ['dns_name1']:
2081 if self.__dict__['ndr_blob'].version == 0x1:
2082 return self.__dict__['ndr_blob'].ctr.other_info.dns_name
2084 return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
2086 elif item in ['dns_name2']:
2087 if self.__dict__['ndr_blob'].version == 0x1:
2088 raise AttributeError(item)
2090 return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
2092 elif item in ['to_be_deleted']:
2093 return self.__dict__['to_be_deleted']
2095 elif item in ['nc_dnstr']:
2096 return self.__dict__['nc_dnstr']
2098 elif item in ['update_flags']:
2099 return self.__dict__['update_flags']
2101 raise AttributeError("Unknown attribute %s" % item)
2103 def is_modified(self):
2104 return (self.update_flags != 0x0)
2106 def set_unmodified(self):
2107 self.__dict__['update_flags'] = 0x0
2110 class SiteLink(object):
2111 """Class defines a site link found under sites
2114 def __init__(self, dnstr):
2117 self.system_flags = 0
2119 self.schedule = None
2120 self.interval = None
2124 '''Debug dump string output of Transport object'''
2126 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
2127 text = text + "\n\toptions=%d" % self.options
2128 text = text + "\n\tsystem_flags=%d" % self.system_flags
2129 text = text + "\n\tcost=%d" % self.cost
2130 text = text + "\n\tinterval=%s" % self.interval
2132 if self.schedule is not None:
2133 text += "\n\tschedule.size=%s" % self.schedule.size
2134 text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
2135 text += ("\n\tschedule.numberOfSchedules=%s" %
2136 self.schedule.numberOfSchedules)
2138 for i, header in enumerate(self.schedule.headerArray):
2139 text += ("\n\tschedule.headerArray[%d].type=%d" %
2141 text += ("\n\tschedule.headerArray[%d].offset=%d" %
2143 text = text + "\n\tschedule.dataArray[%d].slots[ " % i
2144 for slot in self.schedule.dataArray[i].slots:
2145 text = text + "0x%X " % slot
2148 for dnstr in self.site_list:
2149 text = text + "\n\tsite_list=%s" % dnstr
2152 def load_sitelink(self, samdb):
2153 """Given a siteLink object with an prior initialization
2154 for the object's DN, search for the DN and load attributes
2164 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
2165 attrs=attrs, controls=['extended_dn:0'])
2167 except ldb.LdbError as e19:
2168 (enum, estr) = e19.args
2169 raise KCCError("Unable to find SiteLink for (%s) - (%s)" %
2174 if "options" in msg:
2175 self.options = int(msg["options"][0])
2177 if "systemFlags" in msg:
2178 self.system_flags = int(msg["systemFlags"][0])
2181 self.cost = int(msg["cost"][0])
2183 if "replInterval" in msg:
2184 self.interval = int(msg["replInterval"][0])
2186 if "siteList" in msg:
2187 for value in msg["siteList"]:
2188 dsdn = dsdb_Dn(samdb, value)
2189 guid = misc.GUID(dsdn.dn.get_extended_component('GUID'))
2190 if guid not in self.site_list:
2191 self.site_list.append(guid)
2193 if "schedule" in msg:
2194 self.schedule = ndr_unpack(drsblobs.schedule, value)
2196 self.schedule = new_connection_schedule()
2199 class KCCFailedObject(object):
2200 def __init__(self, uuid, failure_count, time_first_failure,
2201 last_result, dns_name):
2203 self.failure_count = failure_count
2204 self.time_first_failure = time_first_failure
2205 self.last_result = last_result
2206 self.dns_name = dns_name
2209 ##################################################
2210 # Global Functions and Variables
2211 ##################################################
2213 def get_dsa_config_rep(dsa):
2214 # Find configuration NC replica for the DSA
2215 for c_rep in dsa.current_rep_table.values():
2216 if c_rep.is_config():
2219 raise KCCError("Unable to find config NC replica for (%s)" %
2223 def sort_dsa_by_guid(dsa1, dsa2):
2224 "use ndr_pack for GUID comparison, as appears correct in some places"""
2225 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
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 :param samdb database
2272 :param site_name origin site (with a DC)
2274 :return a list of sites this site should be covering (for DNS)
2278 server_res = samdb.search(base=samdb.get_config_basedn(),
2279 scope=ldb.SCOPE_SUBTREE,
2280 expression="(&(objectClass=server)"
2281 "(serverReference=*))")
2283 site_res = samdb.search(base=samdb.get_config_basedn(),
2284 scope=ldb.SCOPE_SUBTREE,
2285 expression="(objectClass=site)")
2287 sites_in_use = set()
2289 # Assume server is of form DC,Servers,Site-ABCD because of schema
2290 for msg in server_res:
2291 sites_in_use.add(msg.dn.parent().parent().canonical_str())
2293 if len(sites_in_use) != len(site_res):
2294 # There is a possible uncovered site
2295 sites_uncovered = []
2297 for msg in site_res:
2298 if msg.dn.canonical_str() not in sites_in_use:
2299 sites_uncovered.append(msg)
2301 own_site_dn = "CN={},CN=Sites,{}".format(
2302 ldb.binary_encode(site_name),
2303 ldb.binary_encode(str(samdb.get_config_basedn()))
2306 for site in sites_uncovered:
2307 encoded_dn = ldb.binary_encode(str(site.dn))
2309 # Get a sorted list of all siteLinks featuring the uncovered site
2310 link_res1 = samdb.search(base=samdb.get_config_basedn(),
2311 scope=ldb.SCOPE_SUBTREE, attrs=["cost"],
2312 expression="(&(objectClass=siteLink)"
2313 "(siteList={}))".format(encoded_dn),
2314 controls=["server_sort:1:0:cost"])
2316 # Get a sorted list of all siteLinks connecting this an the
2318 link_res2 = samdb.search(base=samdb.get_config_basedn(),
2319 scope=ldb.SCOPE_SUBTREE, attrs=["cost"],
2320 expression="(&(objectClass=siteLink)"
2321 "(siteList={})(siteList={}))".format(
2324 controls=["server_sort:1:0:cost"])
2326 # Add to list if your link is equal in cost to lowest cost link
2327 if len(link_res1) > 0 and len(link_res2) > 0:
2328 cost1 = int(link_res1[0]['cost'][0])
2329 cost2 = int(link_res2[0]['cost'][0])
2331 site_cover_rdn = site.dn.get_rdn_value()
2332 sites_to_cover.append(site_cover_rdn)
2334 return sites_to_cover