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 Exception("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 schema'''
100 assert self
.nc_type
!= NCType
.unknown
101 return self
.nc_type
== NCType
.schema
104 '''Return True if NC is domain'''
105 assert self
.nc_type
!= NCType
.unknown
106 return self
.nc_type
== NCType
.domain
108 def is_application(self
):
109 '''Return True if NC is application'''
110 assert self
.nc_type
!= NCType
.unknown
111 return self
.nc_type
== NCType
.application
114 '''Return True if NC is config'''
115 assert self
.nc_type
!= NCType
.unknown
116 return self
.nc_type
== NCType
.config
118 def identify_by_basedn(self
, samdb
):
119 """Given an NC object, identify what type is is thru
120 the samdb basedn strings and NC sid value
122 # Invoke loader to initialize guid and more
123 # importantly sid value (sid is used to identify
125 if self
.nc_guid
is None:
128 # We check against schema and config because they
129 # will be the same for all nTDSDSAs in the forest.
130 # That leaves the domain NCs which can be identified
131 # by sid and application NCs as the last identified
132 if self
.nc_dnstr
== str(samdb
.get_schema_basedn()):
133 self
.nc_type
= NCType
.schema
134 elif self
.nc_dnstr
== str(samdb
.get_config_basedn()):
135 self
.nc_type
= NCType
.config
136 elif self
.nc_sid
is not None:
137 self
.nc_type
= NCType
.domain
139 self
.nc_type
= NCType
.application
141 def identify_by_dsa_attr(self
, samdb
, attr
):
142 """Given an NC which has been discovered thru the
143 nTDSDSA database object, determine what type of NC
144 it is (i.e. schema, config, domain, application) via
145 the use of the schema attribute under which the NC
148 :param attr: attr of nTDSDSA object where NC DN appears
150 # If the NC is listed under msDS-HasDomainNCs then
151 # this can only be a domain NC and it is our default
152 # domain for this dsa
153 if attr
== "msDS-HasDomainNCs":
154 self
.nc_type
= NCType
.domain
156 # If the NC is listed under hasPartialReplicaNCs
157 # this is only a domain NC
158 elif attr
== "hasPartialReplicaNCs":
159 self
.nc_type
= NCType
.domain
161 # NCs listed under hasMasterNCs are either
162 # default domain, schema, or config. We
163 # utilize the identify_by_basedn() to
165 elif attr
== "hasMasterNCs":
166 self
.identify_by_basedn(samdb
)
168 # Still unknown (unlikely) but for completeness
169 # and for finally identifying application NCs
170 if self
.nc_type
== NCType
.unknown
:
171 self
.identify_by_basedn(samdb
)
174 class NCReplica(NamingContext
):
175 """Naming context replica that is relative to a specific DSA.
177 This is a more specific form of NamingContext class (inheriting from that
178 class) and it identifies unique attributes of the DSA's replica for a NC.
181 def __init__(self
, dsa_dnstr
, dsa_guid
, nc_dnstr
):
182 """Instantiate a Naming Context Replica
184 :param dsa_guid: GUID of DSA where replica appears
185 :param nc_dnstr: NC dn string
187 self
.rep_dsa_dnstr
= dsa_dnstr
188 self
.rep_dsa_guid
= dsa_guid
189 self
.rep_default
= False # replica for DSA's default domain
190 self
.rep_partial
= False
192 self
.rep_instantiated_flags
= 0
194 self
.rep_fsmo_role_owner
= None
197 self
.rep_repsFrom
= []
199 # The (is present) test is a combination of being
200 # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or
201 # hasPartialReplicaNCs) as well as its replica flags found
202 # thru the msDS-HasInstantiatedNCs. If the NC replica meets
203 # the first enumeration test then this flag is set true
204 self
.rep_present_criteria_one
= False
206 # Call my super class we inherited from
207 NamingContext
.__init
__(self
, nc_dnstr
)
210 '''Debug dump string output of class'''
211 text
= "%s:" % self
.__class
__.__name
__
212 text
= text
+ "\n\tdsa_dnstr=%s" % self
.rep_dsa_dnstr
213 text
= text
+ "\n\tdsa_guid=%s" % self
.rep_dsa_guid
214 text
= text
+ "\n\tdefault=%s" % self
.rep_default
215 text
= text
+ "\n\tro=%s" % self
.rep_ro
216 text
= text
+ "\n\tpartial=%s" % self
.rep_partial
217 text
= text
+ "\n\tpresent=%s" % self
.is_present()
218 text
= text
+ "\n\tfsmo_role_owner=%s" % self
.rep_fsmo_role_owner
220 for rep
in self
.rep_repsFrom
:
221 text
= text
+ "\n%s" % rep
223 return "%s\n%s" % (NamingContext
.__str
__(self
), text
)
225 def set_instantiated_flags(self
, flags
=None):
226 '''Set or clear NC replica instantiated flags'''
228 self
.rep_instantiated_flags
= 0
230 self
.rep_instantiated_flags
= flags
232 def identify_by_dsa_attr(self
, samdb
, attr
):
233 """Given an NC which has been discovered thru the
234 nTDSDSA database object, determine what type of NC
235 replica it is (i.e. partial, read only, default)
237 :param attr: attr of nTDSDSA object where NC DN appears
239 # If the NC was found under hasPartialReplicaNCs
240 # then a partial replica at this dsa
241 if attr
== "hasPartialReplicaNCs":
242 self
.rep_partial
= True
243 self
.rep_present_criteria_one
= True
245 # If the NC is listed under msDS-HasDomainNCs then
246 # this can only be a domain NC and it is the DSA's
248 elif attr
== "msDS-HasDomainNCs":
249 self
.rep_default
= True
251 # NCs listed under hasMasterNCs are either
252 # default domain, schema, or config. We check
253 # against schema and config because they will be
254 # the same for all nTDSDSAs in the forest. That
255 # leaves the default domain NC remaining which
256 # may be different for each nTDSDSAs (and thus
257 # we don't compare agains this samdb's default
259 elif attr
== "hasMasterNCs":
260 self
.rep_present_criteria_one
= True
262 if self
.nc_dnstr
!= str(samdb
.get_schema_basedn()) and \
263 self
.nc_dnstr
!= str(samdb
.get_config_basedn()):
264 self
.rep_default
= True
267 elif attr
== "msDS-hasFullReplicaNCs":
268 self
.rep_present_criteria_one
= True
272 elif attr
== "msDS-hasMasterNCs":
273 self
.rep_present_criteria_one
= True
276 # Now use this DSA attribute to identify the naming
277 # context type by calling the super class method
279 NamingContext
.identify_by_dsa_attr(self
, samdb
, attr
)
281 def is_default(self
):
282 """Whether this is a default domain for the dsa that this NC appears on
284 return self
.rep_default
287 '''Return True if NC replica is read only'''
290 def is_partial(self
):
291 '''Return True if NC replica is partial'''
292 return self
.rep_partial
294 def is_present(self
):
295 """Given an NC replica which has been discovered thru the
296 nTDSDSA database object and populated with replica flags
297 from the msDS-HasInstantiatedNCs; return whether the NC
298 replica is present (true) or if the IT_NC_GOING flag is
299 set then the NC replica is not present (false)
301 if self
.rep_present_criteria_one
and \
302 self
.rep_instantiated_flags
& dsdb
.INSTANCE_TYPE_NC_GOING
== 0:
306 def load_repsFrom(self
, samdb
):
307 """Given an NC replica which has been discovered thru the nTDSDSA
308 database object, load the repsFrom attribute for the local replica.
309 held by my dsa. The repsFrom attribute is not replicated so this
310 attribute is relative only to the local DSA that the samdb exists on
313 res
= samdb
.search(base
=self
.nc_dnstr
, scope
=ldb
.SCOPE_BASE
,
316 except ldb
.LdbError
, (enum
, estr
):
317 raise Exception("Unable to find NC for (%s) - (%s)" %
318 (self
.nc_dnstr
, estr
))
322 # Possibly no repsFrom if this is a singleton DC
323 if "repsFrom" in msg
:
324 for value
in msg
["repsFrom"]:
325 rep
= RepsFromTo(self
.nc_dnstr
,
326 ndr_unpack(drsblobs
.repsFromToBlob
, value
))
327 self
.rep_repsFrom
.append(rep
)
329 def commit_repsFrom(self
, samdb
, ro
=False):
330 """Commit repsFrom to the database"""
332 # XXX - This is not truly correct according to the MS-TECH
333 # docs. To commit a repsFrom we should be using RPCs
334 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
335 # IDL_DRSReplicaDel to affect a repsFrom change.
337 # Those RPCs are missing in samba, so I'll have to
338 # implement them to get this to more accurately
339 # reflect the reference docs. As of right now this
340 # commit to the database will work as its what the
346 for repsFrom
in self
.rep_repsFrom
:
348 # Leave out any to be deleted from
349 # replacement list. Build a list
350 # of to be deleted reps which we will
351 # remove from rep_repsFrom list below
352 if repsFrom
.to_be_deleted
:
353 delreps
.append(repsFrom
)
357 if repsFrom
.is_modified():
358 repsFrom
.set_unmodified()
361 # current (unmodified) elements also get
362 # appended here but no changes will occur
363 # unless something is "to be modified" or
365 newreps
.append(ndr_pack(repsFrom
.ndr_blob
))
367 # Now delete these from our list of rep_repsFrom
368 for repsFrom
in delreps
:
369 self
.rep_repsFrom
.remove(repsFrom
)
372 # Nothing to do if no reps have been modified or
373 # need to be deleted or input option has informed
374 # us to be "readonly" (ro). Leave database
380 m
.dn
= ldb
.Dn(samdb
, self
.nc_dnstr
)
383 ldb
.MessageElement(newreps
, ldb
.FLAG_MOD_REPLACE
, "repsFrom")
388 except ldb
.LdbError
, estr
:
389 raise Exception("Could not set repsFrom for (%s) - (%s)" %
390 (self
.nc_dnstr
, estr
))
392 def load_replUpToDateVector(self
, samdb
):
393 """Given an NC replica which has been discovered thru the nTDSDSA
394 database object, load the replUpToDateVector attribute for the
395 local replica. held by my dsa. The replUpToDateVector
396 attribute is not replicated so this attribute is relative only
397 to the local DSA that the samdb exists on
401 res
= samdb
.search(base
=self
.nc_dnstr
, scope
=ldb
.SCOPE_BASE
,
402 attrs
=["replUpToDateVector"])
404 except ldb
.LdbError
, (enum
, estr
):
405 raise Exception("Unable to find NC for (%s) - (%s)" %
406 (self
.nc_dnstr
, estr
))
410 # Possibly no replUpToDateVector if this is a singleton DC
411 if "replUpToDateVector" in msg
:
412 value
= msg
["replUpToDateVector"][0]
413 blob
= ndr_unpack(drsblobs
.replUpToDateVectorBlob
,
415 if blob
.version
!= 2:
416 # Samba only generates version 2, and this runs locally
417 raise AttributeError("Unexpected replUpToDateVector version %d"
420 self
.rep_replUpToDateVector_cursors
= blob
.ctr
.cursors
422 self
.rep_replUpToDateVector_cursors
= []
424 def dumpstr_to_be_deleted(self
):
425 return '\n'.join(str(x
) for x
in self
.rep_repsFrom
if x
.to_be_deleted
)
427 def dumpstr_to_be_modified(self
):
428 return '\n'.join(str(x
) for x
in self
.rep_repsFrom
if x
.is_modified())
430 def load_fsmo_roles(self
, samdb
):
431 """Given an NC replica which has been discovered thru the nTDSDSA
432 database object, load the fSMORoleOwner attribute.
435 res
= samdb
.search(base
=self
.nc_dnstr
, scope
=ldb
.SCOPE_BASE
,
436 attrs
=["fSMORoleOwner"])
438 except ldb
.LdbError
, (enum
, estr
):
439 raise Exception("Unable to find NC for (%s) - (%s)" %
440 (self
.nc_dnstr
, estr
))
444 # Possibly no fSMORoleOwner
445 if "fSMORoleOwner" in msg
:
446 self
.rep_fsmo_role_owner
= msg
["fSMORoleOwner"]
448 def is_fsmo_role_owner(self
, dsa_dnstr
):
449 if self
.rep_fsmo_role_owner
is not None and \
450 self
.rep_fsmo_role_owner
== dsa_dnstr
:
455 class DirectoryServiceAgent(object):
457 def __init__(self
, dsa_dnstr
):
458 """Initialize DSA class.
460 Class is subsequently fully populated by calling the load_dsa() method
462 :param dsa_dnstr: DN of the nTDSDSA
464 self
.dsa_dnstr
= dsa_dnstr
467 self
.dsa_is_ro
= False
468 self
.dsa_is_istg
= False
470 self
.dsa_behavior
= 0
471 self
.default_dnstr
= None # default domain dn string for dsa
473 # NCReplicas for this dsa that are "present"
474 # Indexed by DN string of naming context
475 self
.current_rep_table
= {}
477 # NCReplicas for this dsa that "should be present"
478 # Indexed by DN string of naming context
479 self
.needed_rep_table
= {}
481 # NTDSConnections for this dsa. These are current
482 # valid connections that are committed or pending a commit
483 # in the database. Indexed by DN string of connection
484 self
.connect_table
= {}
487 '''Debug dump string output of class'''
489 text
= "%s:" % self
.__class
__.__name
__
490 if self
.dsa_dnstr
is not None:
491 text
= text
+ "\n\tdsa_dnstr=%s" % self
.dsa_dnstr
492 if self
.dsa_guid
is not None:
493 text
= text
+ "\n\tdsa_guid=%s" % str(self
.dsa_guid
)
494 if self
.dsa_ivid
is not None:
495 text
= text
+ "\n\tdsa_ivid=%s" % str(self
.dsa_ivid
)
497 text
= text
+ "\n\tro=%s" % self
.is_ro()
498 text
= text
+ "\n\tgc=%s" % self
.is_gc()
499 text
= text
+ "\n\tistg=%s" % self
.is_istg()
501 text
= text
+ "\ncurrent_replica_table:"
502 text
= text
+ "\n%s" % self
.dumpstr_current_replica_table()
503 text
= text
+ "\nneeded_replica_table:"
504 text
= text
+ "\n%s" % self
.dumpstr_needed_replica_table()
505 text
= text
+ "\nconnect_table:"
506 text
= text
+ "\n%s" % self
.dumpstr_connect_table()
510 def get_current_replica(self
, nc_dnstr
):
511 return self
.current_rep_table
.get(nc_dnstr
)
514 '''Returns True if dsa is intersite topology generator for it's site'''
515 # The KCC on an RODC always acts as an ISTG for itself
516 return self
.dsa_is_istg
or self
.dsa_is_ro
519 '''Returns True if dsa a read only domain controller'''
520 return self
.dsa_is_ro
523 '''Returns True if dsa hosts a global catalog'''
524 if (self
.options
& dsdb
.DS_NTDSDSA_OPT_IS_GC
) != 0:
528 def is_minimum_behavior(self
, version
):
529 """Is dsa at minimum windows level greater than or equal to (version)
531 :param version: Windows version to test against
532 (e.g. DS_DOMAIN_FUNCTION_2008)
534 if self
.dsa_behavior
>= version
:
538 def is_translate_ntdsconn_disabled(self
):
539 """Whether this allows NTDSConnection translation in its options."""
540 if (self
.options
& dsdb
.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE
) != 0:
544 def get_rep_tables(self
):
545 """Return DSA current and needed replica tables
547 return self
.current_rep_table
, self
.needed_rep_table
549 def get_parent_dnstr(self
):
550 """Get the parent DN string of this object."""
551 head
, sep
, tail
= self
.dsa_dnstr
.partition(',')
554 def load_dsa(self
, samdb
):
555 """Load a DSA from the samdb.
557 Prior initialization has given us the DN of the DSA that we are to
558 load. This method initializes all other attributes, including loading
559 the NC replica table for this DSA.
561 attrs
= ["objectGUID",
565 "msDS-Behavior-Version"]
567 res
= samdb
.search(base
=self
.dsa_dnstr
, scope
=ldb
.SCOPE_BASE
,
570 except ldb
.LdbError
, (enum
, estr
):
571 raise Exception("Unable to find nTDSDSA for (%s) - (%s)" %
572 (self
.dsa_dnstr
, estr
))
575 self
.dsa_guid
= misc
.GUID(samdb
.schema_format_value("objectGUID",
576 msg
["objectGUID"][0]))
578 # RODCs don't originate changes and thus have no invocationId,
579 # therefore we must check for existence first
580 if "invocationId" in msg
:
581 self
.dsa_ivid
= misc
.GUID(samdb
.schema_format_value("objectGUID",
582 msg
["invocationId"][0]))
585 self
.options
= int(msg
["options"][0])
587 if "msDS-isRODC" in msg
and msg
["msDS-isRODC"][0] == "TRUE":
588 self
.dsa_is_ro
= True
590 self
.dsa_is_ro
= False
592 if "msDS-Behavior-Version" in msg
:
593 self
.dsa_behavior
= int(msg
['msDS-Behavior-Version'][0])
595 # Load the NC replicas that are enumerated on this dsa
596 self
.load_current_replica_table(samdb
)
598 # Load the nTDSConnection that are enumerated on this dsa
599 self
.load_connection_table(samdb
)
601 def load_current_replica_table(self
, samdb
):
602 """Method to load the NC replica's listed for DSA object.
604 This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs,
605 hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, and
606 msDS-HasInstantiatedNCs) to determine complete list of NC replicas that
607 are enumerated for the DSA. Once a NC replica is loaded it is
608 identified (schema, config, etc) and the other replica attributes
609 (partial, ro, etc) are determined.
611 :param samdb: database to query for DSA replica list
614 # not RODC - default, config, schema (old style)
616 # not RODC - default, config, schema, app NCs
618 # domain NC partial replicas
619 "hasPartialReplicaNCs",
622 # RODC only - default, config, schema, app NCs
623 "msDS-hasFullReplicaNCs",
624 # Identifies if replica is coming, going, or stable
625 "msDS-HasInstantiatedNCs"
628 res
= samdb
.search(base
=self
.dsa_dnstr
, scope
=ldb
.SCOPE_BASE
,
631 except ldb
.LdbError
, (enum
, estr
):
632 raise Exception("Unable to find nTDSDSA NCs for (%s) - (%s)" %
633 (self
.dsa_dnstr
, estr
))
635 # The table of NCs for the dsa we are searching
638 # We should get one response to our query here for
639 # the ntds that we requested
642 # Our response will contain a number of elements including
643 # the dn of the dsa as well as elements for each
644 # attribute (e.g. hasMasterNCs). Each of these elements
645 # is a dictonary list which we retrieve the keys for and
646 # then iterate over them
647 for k
in res
[0].keys():
651 # For each attribute type there will be one or more DNs
652 # listed. For instance DCs normally have 3 hasMasterNCs
654 for value
in res
[0][k
]:
655 # Turn dn into a dsdb_Dn so we can use
656 # its methods to parse a binary DN
657 dsdn
= dsdb_Dn(samdb
, value
)
658 flags
= dsdn
.get_binary_integer()
661 if not dnstr
in tmp_table
:
662 rep
= NCReplica(self
.dsa_dnstr
, self
.dsa_guid
, dnstr
)
663 tmp_table
[dnstr
] = rep
665 rep
= tmp_table
[dnstr
]
667 if k
== "msDS-HasInstantiatedNCs":
668 rep
.set_instantiated_flags(flags
)
671 rep
.identify_by_dsa_attr(samdb
, k
)
673 # if we've identified the default domain NC
674 # then save its DN string
676 self
.default_dnstr
= dnstr
678 raise Exception("No nTDSDSA NCs for (%s)" % self
.dsa_dnstr
)
680 # Assign our newly built NC replica table to this dsa
681 self
.current_rep_table
= tmp_table
683 def add_needed_replica(self
, rep
):
684 """Method to add a NC replica that "should be present" to the
687 self
.needed_rep_table
[rep
.nc_dnstr
] = rep
689 def load_connection_table(self
, samdb
):
690 """Method to load the nTDSConnections listed for DSA object.
692 :param samdb: database to query for DSA connection list
695 res
= samdb
.search(base
=self
.dsa_dnstr
,
696 scope
=ldb
.SCOPE_SUBTREE
,
697 expression
="(objectClass=nTDSConnection)")
699 except ldb
.LdbError
, (enum
, estr
):
700 raise Exception("Unable to find nTDSConnection for (%s) - (%s)" %
701 (self
.dsa_dnstr
, estr
))
707 if dnstr
in self
.connect_table
:
710 connect
= NTDSConnection(dnstr
)
712 connect
.load_connection(samdb
)
713 self
.connect_table
[dnstr
] = connect
715 def commit_connections(self
, samdb
, ro
=False):
716 """Method to commit any uncommitted nTDSConnections
717 modifications that are in our table. These would be
718 identified connections that are marked to be added or
721 :param samdb: database to commit DSA connection list to
722 :param ro: if (true) then peform internal operations but
723 do not write to the database (readonly)
727 for dnstr
, connect
in self
.connect_table
.items():
728 if connect
.to_be_added
:
729 connect
.commit_added(samdb
, ro
)
731 if connect
.to_be_modified
:
732 connect
.commit_modified(samdb
, ro
)
734 if connect
.to_be_deleted
:
735 connect
.commit_deleted(samdb
, ro
)
736 delconn
.append(dnstr
)
738 # Now delete the connection from the table
739 for dnstr
in delconn
:
740 del self
.connect_table
[dnstr
]
742 def add_connection(self
, dnstr
, connect
):
743 assert dnstr
not in self
.connect_table
744 self
.connect_table
[dnstr
] = connect
746 def get_connection_by_from_dnstr(self
, from_dnstr
):
747 """Scan DSA nTDSConnection table and return connection
748 with a "fromServer" dn string equivalent to method
751 :param from_dnstr: search for this from server entry
754 for connect
in self
.connect_table
.values():
755 if connect
.get_from_dnstr() == from_dnstr
:
756 answer
.append(connect
)
760 def dumpstr_current_replica_table(self
):
761 '''Debug dump string output of current replica table'''
762 return '\n'.join(str(x
) for x
in self
.current_rep_table
)
764 def dumpstr_needed_replica_table(self
):
765 '''Debug dump string output of needed replica table'''
766 return '\n'.join(str(x
) for x
in self
.needed_rep_table
)
768 def dumpstr_connect_table(self
):
769 '''Debug dump string output of connect table'''
770 return '\n'.join(str(x
) for x
in self
.connect_table
)
772 def new_connection(self
, options
, flags
, transport
, from_dnstr
, sched
):
773 """Set up a new connection for the DSA based on input
774 parameters. Connection will be added to the DSA
775 connect_table and will be marked as "to be added" pending
776 a call to commit_connections()
778 dnstr
= "CN=%s," % str(uuid
.uuid4()) + self
.dsa_dnstr
780 connect
= NTDSConnection(dnstr
)
781 connect
.to_be_added
= True
782 connect
.enabled
= True
783 connect
.from_dnstr
= from_dnstr
784 connect
.options
= options
785 connect
.flags
= flags
787 if transport
is not None:
788 connect
.transport_dnstr
= transport
.dnstr
789 connect
.transport_guid
= transport
.guid
791 if sched
is not None:
792 connect
.schedule
= sched
794 # Create schedule. Attribute valuse set according to MS-TECH
795 # intrasite connection creation document
796 connect
.schedule
= new_connection_schedule()
798 self
.add_connection(dnstr
, connect
)
802 class NTDSConnection(object):
803 """Class defines a nTDSConnection found under a DSA
805 def __init__(self
, dnstr
):
810 self
.to_be_added
= False # new connection needs to be added
811 self
.to_be_deleted
= False # old connection needs to be deleted
812 self
.to_be_modified
= False
814 self
.system_flags
= 0
815 self
.transport_dnstr
= None
816 self
.transport_guid
= None
817 self
.from_dnstr
= None
821 '''Debug dump string output of NTDSConnection object'''
823 text
= "%s:\n\tdn=%s" % (self
.__class
__.__name
__, self
.dnstr
)
824 text
= text
+ "\n\tenabled=%s" % self
.enabled
825 text
= text
+ "\n\tto_be_added=%s" % self
.to_be_added
826 text
= text
+ "\n\tto_be_deleted=%s" % self
.to_be_deleted
827 text
= text
+ "\n\tto_be_modified=%s" % self
.to_be_modified
828 text
= text
+ "\n\toptions=0x%08X" % self
.options
829 text
= text
+ "\n\tsystem_flags=0x%08X" % self
.system_flags
830 text
= text
+ "\n\twhenCreated=%d" % self
.whenCreated
831 text
= text
+ "\n\ttransport_dn=%s" % self
.transport_dnstr
833 if self
.guid
is not None:
834 text
= text
+ "\n\tguid=%s" % str(self
.guid
)
836 if self
.transport_guid
is not None:
837 text
= text
+ "\n\ttransport_guid=%s" % str(self
.transport_guid
)
839 text
= text
+ "\n\tfrom_dn=%s" % self
.from_dnstr
841 if self
.schedule
is not None:
842 text
+= "\n\tschedule.size=%s" % self
.schedule
.size
843 text
+= "\n\tschedule.bandwidth=%s" % self
.schedule
.bandwidth
844 text
+= ("\n\tschedule.numberOfSchedules=%s" %
845 self
.schedule
.numberOfSchedules
)
847 for i
, header
in enumerate(self
.schedule
.headerArray
):
848 text
+= ("\n\tschedule.headerArray[%d].type=%d" %
850 text
+= ("\n\tschedule.headerArray[%d].offset=%d" %
852 text
+= "\n\tschedule.dataArray[%d].slots[ " % i
853 for slot
in self
.schedule
.dataArray
[i
].slots
:
854 text
= text
+ "0x%X " % slot
859 def load_connection(self
, samdb
):
860 """Given a NTDSConnection object with an prior initialization
861 for the object's DN, search for the DN and load attributes
873 res
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
,
876 except ldb
.LdbError
, (enum
, estr
):
877 raise Exception("Unable to find nTDSConnection for (%s) - (%s)" %
883 self
.options
= int(msg
["options"][0])
885 if "enabledConnection" in msg
:
886 if msg
["enabledConnection"][0].upper().lstrip().rstrip() == "TRUE":
889 if "systemFlags" in msg
:
890 self
.system_flags
= int(msg
["systemFlags"][0])
892 if "objectGUID" in msg
:
894 misc
.GUID(samdb
.schema_format_value("objectGUID",
895 msg
["objectGUID"][0]))
897 if "transportType" in msg
:
898 dsdn
= dsdb_Dn(samdb
, msg
["transportType"][0])
899 self
.load_connection_transport(samdb
, str(dsdn
.dn
))
901 if "schedule" in msg
:
902 self
.schedule
= ndr_unpack(drsblobs
.schedule
, msg
["schedule"][0])
904 if "whenCreated" in msg
:
905 self
.whenCreated
= ldb
.string_to_time(msg
["whenCreated"][0])
907 if "fromServer" in msg
:
908 dsdn
= dsdb_Dn(samdb
, msg
["fromServer"][0])
909 self
.from_dnstr
= str(dsdn
.dn
)
910 assert self
.from_dnstr
is not None
912 def load_connection_transport(self
, samdb
, tdnstr
):
913 """Given a NTDSConnection object which enumerates a transport
914 DN, load the transport information for the connection object
916 :param tdnstr: transport DN to load
918 attrs
= ["objectGUID"]
920 res
= samdb
.search(base
=tdnstr
,
921 scope
=ldb
.SCOPE_BASE
, attrs
=attrs
)
923 except ldb
.LdbError
, (enum
, estr
):
924 raise Exception("Unable to find transport (%s) - (%s)" %
927 if "objectGUID" in res
[0]:
929 self
.transport_dnstr
= tdnstr
930 self
.transport_guid
= \
931 misc
.GUID(samdb
.schema_format_value("objectGUID",
932 msg
["objectGUID"][0]))
933 assert self
.transport_dnstr
is not None
934 assert self
.transport_guid
is not None
936 def commit_deleted(self
, samdb
, ro
=False):
937 """Local helper routine for commit_connections() which
938 handles committed connections that are to be deleted from
939 the database database
941 assert self
.to_be_deleted
942 self
.to_be_deleted
= False
944 # No database modification requested
949 samdb
.delete(self
.dnstr
)
950 except ldb
.LdbError
, (enum
, estr
):
951 raise Exception("Could not delete nTDSConnection for (%s) - (%s)" %
954 def commit_added(self
, samdb
, ro
=False):
955 """Local helper routine for commit_connections() which
956 handles committed connections that are to be added to the
959 assert self
.to_be_added
960 self
.to_be_added
= False
962 # No database modification requested
966 # First verify we don't have this entry to ensure nothing
967 # is programatically amiss
970 msg
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
)
974 except ldb
.LdbError
, (enum
, estr
):
975 if enum
!= ldb
.ERR_NO_SUCH_OBJECT
:
976 raise Exception("Unable to search for (%s) - (%s)" %
979 raise Exception("nTDSConnection for (%s) already exists!" %
987 # Prepare a message for adding to the samdb
989 m
.dn
= ldb
.Dn(samdb
, self
.dnstr
)
992 ldb
.MessageElement("nTDSConnection", ldb
.FLAG_MOD_ADD
,
994 m
["showInAdvancedViewOnly"] = \
995 ldb
.MessageElement("TRUE", ldb
.FLAG_MOD_ADD
,
996 "showInAdvancedViewOnly")
997 m
["enabledConnection"] = \
998 ldb
.MessageElement(enablestr
, ldb
.FLAG_MOD_ADD
,
1001 ldb
.MessageElement(self
.from_dnstr
, ldb
.FLAG_MOD_ADD
, "fromServer")
1003 ldb
.MessageElement(str(self
.options
), ldb
.FLAG_MOD_ADD
, "options")
1004 m
["systemFlags"] = \
1005 ldb
.MessageElement(str(self
.system_flags
), ldb
.FLAG_MOD_ADD
,
1008 if self
.transport_dnstr
is not None:
1009 m
["transportType"] = \
1010 ldb
.MessageElement(str(self
.transport_dnstr
), ldb
.FLAG_MOD_ADD
,
1013 if self
.schedule
is not None:
1015 ldb
.MessageElement(ndr_pack(self
.schedule
),
1016 ldb
.FLAG_MOD_ADD
, "schedule")
1019 except ldb
.LdbError
, (enum
, estr
):
1020 raise Exception("Could not add nTDSConnection for (%s) - (%s)" %
1023 def commit_modified(self
, samdb
, ro
=False):
1024 """Local helper routine for commit_connections() which
1025 handles committed connections that are to be modified to the
1028 assert self
.to_be_modified
1029 self
.to_be_modified
= False
1031 # No database modification requested
1035 # First verify we have this entry to ensure nothing
1036 # is programatically amiss
1038 # we don't use the search result, but it tests the status
1039 # of self.dnstr in the database.
1040 samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
)
1042 except ldb
.LdbError
, (enum
, estr
):
1043 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
1044 raise KCCError("nTDSConnection for (%s) doesn't exist!" %
1046 raise KccError("Unable to search for (%s) - (%s)" %
1054 # Prepare a message for modifying the samdb
1056 m
.dn
= ldb
.Dn(samdb
, self
.dnstr
)
1058 m
["enabledConnection"] = \
1059 ldb
.MessageElement(enablestr
, ldb
.FLAG_MOD_REPLACE
,
1060 "enabledConnection")
1062 ldb
.MessageElement(self
.from_dnstr
, ldb
.FLAG_MOD_REPLACE
,
1065 ldb
.MessageElement(str(self
.options
), ldb
.FLAG_MOD_REPLACE
,
1067 m
["systemFlags"] = \
1068 ldb
.MessageElement(str(self
.system_flags
), ldb
.FLAG_MOD_REPLACE
,
1071 if self
.transport_dnstr
is not None:
1072 m
["transportType"] = \
1073 ldb
.MessageElement(str(self
.transport_dnstr
),
1074 ldb
.FLAG_MOD_REPLACE
, "transportType")
1076 m
["transportType"] = \
1077 ldb
.MessageElement([], ldb
.FLAG_MOD_DELETE
, "transportType")
1079 if self
.schedule
is not None:
1081 ldb
.MessageElement(ndr_pack(self
.schedule
),
1082 ldb
.FLAG_MOD_REPLACE
, "schedule")
1085 ldb
.MessageElement([], ldb
.FLAG_MOD_DELETE
, "schedule")
1088 except ldb
.LdbError
, (enum
, estr
):
1089 raise Exception("Could not modify nTDSConnection for (%s) - (%s)" %
1092 def set_modified(self
, truefalse
):
1093 self
.to_be_modified
= truefalse
1095 def set_added(self
, truefalse
):
1096 self
.to_be_added
= truefalse
1098 def set_deleted(self
, truefalse
):
1099 self
.to_be_deleted
= truefalse
1101 def is_schedule_minimum_once_per_week(self
):
1102 """Returns True if our schedule includes at least one
1103 replication interval within the week. False otherwise
1105 # replinfo schedule is None means "always", while
1106 # NTDSConnection schedule is None means "never".
1107 if self
.schedule
is None or self
.schedule
.dataArray
[0] is None:
1110 for slot
in self
.schedule
.dataArray
[0].slots
:
1111 if (slot
& 0x0F) != 0x0:
1115 def is_equivalent_schedule(self
, sched
):
1116 """Returns True if our schedule is equivalent to the input
1117 comparison schedule.
1119 :param shed: schedule to compare to
1121 if self
.schedule
is not None:
1127 if ((self
.schedule
.size
!= sched
.size
or
1128 self
.schedule
.bandwidth
!= sched
.bandwidth
or
1129 self
.schedule
.numberOfSchedules
!= sched
.numberOfSchedules
)):
1132 for i
, header
in enumerate(self
.schedule
.headerArray
):
1134 if self
.schedule
.headerArray
[i
].type != sched
.headerArray
[i
].type:
1137 if self
.schedule
.headerArray
[i
].offset
!= \
1138 sched
.headerArray
[i
].offset
:
1141 for a
, b
in zip(self
.schedule
.dataArray
[i
].slots
,
1142 sched
.dataArray
[i
].slots
):
1147 def is_rodc_topology(self
):
1148 """Returns True if NTDS Connection specifies RODC
1151 if self
.options
& dsdb
.NTDSCONN_OPT_RODC_TOPOLOGY
== 0:
1155 def is_generated(self
):
1156 """Returns True if NTDS Connection was generated by the
1157 KCC topology algorithm as opposed to set by the administrator
1159 if self
.options
& dsdb
.NTDSCONN_OPT_IS_GENERATED
== 0:
1163 def is_override_notify_default(self
):
1164 """Returns True if NTDS Connection should override notify default
1166 if self
.options
& dsdb
.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT
== 0:
1170 def is_use_notify(self
):
1171 """Returns True if NTDS Connection should use notify
1173 if self
.options
& dsdb
.NTDSCONN_OPT_USE_NOTIFY
== 0:
1177 def is_twoway_sync(self
):
1178 """Returns True if NTDS Connection should use twoway sync
1180 if self
.options
& dsdb
.NTDSCONN_OPT_TWOWAY_SYNC
== 0:
1184 def is_intersite_compression_disabled(self
):
1185 """Returns True if NTDS Connection intersite compression
1188 if self
.options
& dsdb
.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
== 0:
1192 def is_user_owned_schedule(self
):
1193 """Returns True if NTDS Connection has a user owned schedule
1195 if self
.options
& dsdb
.NTDSCONN_OPT_USER_OWNED_SCHEDULE
== 0:
1199 def is_enabled(self
):
1200 """Returns True if NTDS Connection is enabled
1204 def get_from_dnstr(self
):
1205 '''Return fromServer dn string attribute'''
1206 return self
.from_dnstr
1209 class Partition(NamingContext
):
1210 """A naming context discovered thru Partitions DN of the config schema.
1212 This is a more specific form of NamingContext class (inheriting from that
1213 class) and it identifies unique attributes enumerated in the Partitions
1214 such as which nTDSDSAs are cross referenced for replicas
1216 def __init__(self
, partstr
):
1217 self
.partstr
= partstr
1219 self
.system_flags
= 0
1220 self
.rw_location_list
= []
1221 self
.ro_location_list
= []
1223 # We don't have enough info to properly
1224 # fill in the naming context yet. We'll get that
1225 # fully set up with load_partition().
1226 NamingContext
.__init
__(self
, None)
1228 def load_partition(self
, samdb
):
1229 """Given a Partition class object that has been initialized with its
1230 partition dn string, load the partition from the sam database, identify
1231 the type of the partition (schema, domain, etc) and record the list of
1232 nTDSDSAs that appear in the cross reference attributes
1233 msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations.
1235 :param samdb: sam database to load partition from
1240 "msDS-NC-Replica-Locations",
1241 "msDS-NC-RO-Replica-Locations"]
1243 res
= samdb
.search(base
=self
.partstr
, scope
=ldb
.SCOPE_BASE
,
1246 except ldb
.LdbError
, (enum
, estr
):
1247 raise Exception("Unable to find partition for (%s) - (%s)" % (
1248 self
.partstr
, estr
))
1251 for k
in msg
.keys():
1256 if msg
[k
][0].upper().lstrip().rstrip() == "TRUE":
1259 self
.enabled
= False
1262 if k
== "systemFlags":
1263 self
.system_flags
= int(msg
[k
][0])
1266 for value
in msg
[k
]:
1267 dsdn
= dsdb_Dn(samdb
, value
)
1268 dnstr
= str(dsdn
.dn
)
1271 self
.nc_dnstr
= dnstr
1274 if k
== "msDS-NC-Replica-Locations":
1275 self
.rw_location_list
.append(dnstr
)
1278 if k
== "msDS-NC-RO-Replica-Locations":
1279 self
.ro_location_list
.append(dnstr
)
1282 # Now identify what type of NC this partition
1284 self
.identify_by_basedn(samdb
)
1286 def is_enabled(self
):
1287 """Returns True if partition is enabled
1289 return self
.is_enabled
1291 def is_foreign(self
):
1292 """Returns True if this is not an Active Directory NC in our
1293 forest but is instead something else (e.g. a foreign NC)
1295 if (self
.system_flags
& dsdb
.SYSTEM_FLAG_CR_NTDS_NC
) == 0:
1300 def should_be_present(self
, target_dsa
):
1301 """Tests whether this partition should have an NC replica
1302 on the target dsa. This method returns a tuple of
1303 needed=True/False, ro=True/False, partial=True/False
1305 :param target_dsa: should NC be present on target dsa
1310 # If this is the config, schema, or default
1311 # domain NC for the target dsa then it should
1313 needed
= (self
.nc_type
== NCType
.config
or
1314 self
.nc_type
== NCType
.schema
or
1315 (self
.nc_type
== NCType
.domain
and
1316 self
.nc_dnstr
== target_dsa
.default_dnstr
))
1318 # A writable replica of an application NC should be present
1319 # if there a cross reference to the target DSA exists. Depending
1320 # on whether the DSA is ro we examine which type of cross reference
1321 # to look for (msDS-NC-Replica-Locations or
1322 # msDS-NC-RO-Replica-Locations
1323 if self
.nc_type
== NCType
.application
:
1324 if target_dsa
.is_ro():
1325 if target_dsa
.dsa_dnstr
in self
.ro_location_list
:
1328 if target_dsa
.dsa_dnstr
in self
.rw_location_list
:
1331 # If the target dsa is a gc then a partial replica of a
1332 # domain NC (other than the DSAs default domain) should exist
1333 # if there is also a cross reference for the DSA
1334 if (target_dsa
.is_gc() and
1335 self
.nc_type
== NCType
.domain
and
1336 self
.nc_dnstr
!= target_dsa
.default_dnstr
and
1337 (target_dsa
.dsa_dnstr
in self
.ro_location_list
or
1338 target_dsa
.dsa_dnstr
in self
.rw_location_list
)):
1342 # partial NCs are always readonly
1343 if needed
and (target_dsa
.is_ro() or partial
):
1346 return needed
, ro
, partial
1349 '''Debug dump string output of class'''
1350 text
= "%s" % NamingContext
.__str
__(self
)
1351 text
= text
+ "\n\tpartdn=%s" % self
.partstr
1352 for k
in self
.rw_location_list
:
1353 text
= text
+ "\n\tmsDS-NC-Replica-Locations=%s" % k
1354 for k
in self
.ro_location_list
:
1355 text
= text
+ "\n\tmsDS-NC-RO-Replica-Locations=%s" % k
1360 """An individual site object discovered thru the configuration
1361 naming context. Contains all DSAs that exist within the site
1363 def __init__(self
, site_dnstr
, nt_now
):
1364 self
.site_dnstr
= site_dnstr
1365 self
.site_guid
= None
1366 self
.site_options
= 0
1367 self
.site_topo_generator
= None
1368 self
.site_topo_failover
= 0 # appears to be in minutes
1370 self
.rw_dsa_table
= {}
1371 self
.nt_now
= nt_now
1373 def load_site(self
, samdb
):
1374 """Loads the NTDS Site Settions options attribute for the site
1375 as well as querying and loading all DSAs that appear within
1378 ssdn
= "CN=NTDS Site Settings,%s" % self
.site_dnstr
1380 "interSiteTopologyFailover",
1381 "interSiteTopologyGenerator"]
1383 res
= samdb
.search(base
=ssdn
, scope
=ldb
.SCOPE_BASE
,
1385 self_res
= samdb
.search(base
=self
.site_dnstr
, scope
=ldb
.SCOPE_BASE
,
1386 attrs
=['objectGUID'])
1387 except ldb
.LdbError
, (enum
, estr
):
1388 raise Exception("Unable to find site settings for (%s) - (%s)" %
1392 if "options" in msg
:
1393 self
.site_options
= int(msg
["options"][0])
1395 if "interSiteTopologyGenerator" in msg
:
1396 self
.site_topo_generator
= \
1397 str(msg
["interSiteTopologyGenerator"][0])
1399 if "interSiteTopologyFailover" in msg
:
1400 self
.site_topo_failover
= int(msg
["interSiteTopologyFailover"][0])
1403 if "objectGUID" in msg
:
1404 self
.site_guid
= misc
.GUID(samdb
.schema_format_value("objectGUID",
1405 msg
["objectGUID"][0]))
1407 self
.load_all_dsa(samdb
)
1409 def load_all_dsa(self
, samdb
):
1410 """Discover all nTDSDSA thru the sites entry and
1411 instantiate and load the DSAs. Each dsa is inserted
1412 into the dsa_table by dn string.
1415 res
= samdb
.search(self
.site_dnstr
,
1416 scope
=ldb
.SCOPE_SUBTREE
,
1417 expression
="(objectClass=nTDSDSA)")
1418 except ldb
.LdbError
, (enum
, estr
):
1419 raise Exception("Unable to find nTDSDSAs - (%s)" % estr
)
1425 if dnstr
in self
.dsa_table
:
1428 dsa
= DirectoryServiceAgent(dnstr
)
1432 # Assign this dsa to my dsa table
1433 # and index by dsa dn
1434 self
.dsa_table
[dnstr
] = dsa
1436 self
.rw_dsa_table
[dnstr
] = dsa
1438 def get_dsa_by_guidstr(self
, guidstr
): # XXX unused
1439 for dsa
in self
.dsa_table
.values():
1440 if str(dsa
.dsa_guid
) == guidstr
:
1444 def get_dsa(self
, dnstr
):
1445 """Return a previously loaded DSA object by consulting
1446 the sites dsa_table for the provided DSA dn string
1448 :return: None if DSA doesn't exist
1450 return self
.dsa_table
.get(dnstr
)
1452 def select_istg(self
, samdb
, mydsa
, ro
):
1453 """Determine if my DC should be an intersite topology
1454 generator. If my DC is the istg and is both a writeable
1455 DC and the database is opened in write mode then we perform
1456 an originating update to set the interSiteTopologyGenerator
1457 attribute in the NTDS Site Settings object. An RODC always
1458 acts as an ISTG for itself.
1460 # The KCC on an RODC always acts as an ISTG for itself
1462 mydsa
.dsa_is_istg
= True
1463 self
.site_topo_generator
= mydsa
.dsa_dnstr
1466 c_rep
= get_dsa_config_rep(mydsa
)
1468 # Load repsFrom and replUpToDateVector if not already loaded
1469 # so we can get the current state of the config replica and
1470 # whether we are getting updates from the istg
1471 c_rep
.load_repsFrom(samdb
)
1473 c_rep
.load_replUpToDateVector(samdb
)
1475 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1476 # First, the KCC on a writable DC determines whether it acts
1477 # as an ISTG for its site
1479 # Let s be the object such that s!lDAPDisplayName = nTDSDSA
1480 # and classSchema in s!objectClass.
1482 # Let D be the sequence of objects o in the site of the local
1483 # DC such that o!objectCategory = s. D is sorted in ascending
1484 # order by objectGUID.
1486 # Which is a fancy way of saying "sort all the nTDSDSA objects
1487 # in the site by guid in ascending order". Place sorted list
1489 D_sort
= sorted(self
.rw_dsa_table
.values(), cmp=sort_dsa_by_guid
)
1491 # double word number of 100 nanosecond intervals since 1600s
1493 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1494 # if o!interSiteTopologyFailover is 0 or has no value.
1496 # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1497 # so it appears we have to turn f into the same interval
1499 # interSiteTopologyFailover (if set) appears to be in minutes
1500 # so we'll need to convert to senconds and then 100 nanosecond
1502 # XXX [MS-ADTS] 6.2.2.3.1 says it is seconds, not minutes.
1504 # 10,000,000 is number of 100 nanosecond intervals in a second
1505 if self
.site_topo_failover
== 0:
1506 f
= 2 * 60 * 60 * 10000000
1508 f
= self
.site_topo_failover
* 60 * 10000000
1510 # Let o be the site settings object for the site of the local
1511 # DC, or NULL if no such o exists.
1512 d_dsa
= self
.dsa_table
.get(self
.site_topo_generator
)
1514 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1515 # If o != NULL and o!interSiteTopologyGenerator is not the
1516 # nTDSDSA object for the local DC and
1517 # o!interSiteTopologyGenerator is an element dj of sequence D:
1519 if d_dsa
is not None and d_dsa
is not mydsa
:
1520 # From MS-ADTS 6.2.2.3.1 ISTG Selection:
1521 # Let c be the cursor in the replUpToDateVector variable
1522 # associated with the NC replica of the config NC such
1523 # that c.uuidDsa = dj!invocationId. If no such c exists
1524 # (No evidence of replication from current ITSG):
1528 # Else if the current time < c.timeLastSyncSuccess - f
1529 # (Evidence of time sync problem on current ISTG):
1533 # Else (Evidence of replication from current ITSG):
1535 # Let t = c.timeLastSyncSuccess.
1537 # last_success appears to be a double word containing
1538 # number of 100 nanosecond intervals since the 1600s
1539 j_idx
= D_sort
.index(d_dsa
)
1542 for cursor
in c_rep
.rep_replUpToDateVector_cursors
:
1543 if d_dsa
.dsa_ivid
== cursor
.source_dsa_invocation_id
:
1551 #XXX doc says current time < c.timeLastSyncSuccess - f
1552 # which is true only if f is negative or clocks are wrong.
1553 # f is not negative in the default case (2 hours).
1554 elif self
.nt_now
- cursor
.last_sync_success
> f
:
1559 t_time
= cursor
.last_sync_success
1561 # Otherwise (Nominate local DC as ISTG):
1562 # Let i be the integer such that di is the nTDSDSA
1563 # object for the local DC.
1564 # Let t = the current time.
1566 i_idx
= D_sort
.index(mydsa
)
1567 t_time
= self
.nt_now
1569 # Compute a function that maintains the current ISTG if
1570 # it is alive, cycles through other candidates if not.
1572 # Let k be the integer (i + ((current time - t) /
1573 # o!interSiteTopologyFailover)) MOD |D|.
1575 # Note: We don't want to divide by zero here so they must
1576 # have meant "f" instead of "o!interSiteTopologyFailover"
1577 k_idx
= (i_idx
+ ((self
.nt_now
- t_time
) / f
)) % len(D_sort
)
1579 # The local writable DC acts as an ISTG for its site if and
1580 # only if dk is the nTDSDSA object for the local DC. If the
1581 # local DC does not act as an ISTG, the KCC skips the
1582 # remainder of this task.
1583 d_dsa
= D_sort
[k_idx
]
1584 d_dsa
.dsa_is_istg
= True
1586 # Update if we are the ISTG, otherwise return
1587 if d_dsa
is not mydsa
:
1591 if self
.site_topo_generator
== mydsa
.dsa_dnstr
:
1594 self
.site_topo_generator
= mydsa
.dsa_dnstr
1596 # If readonly database then do not perform a
1601 # Perform update to the samdb
1602 ssdn
= "CN=NTDS Site Settings,%s" % self
.site_dnstr
1605 m
.dn
= ldb
.Dn(samdb
, ssdn
)
1607 m
["interSiteTopologyGenerator"] = \
1608 ldb
.MessageElement(mydsa
.dsa_dnstr
, ldb
.FLAG_MOD_REPLACE
,
1609 "interSiteTopologyGenerator")
1613 except ldb
.LdbError
, estr
:
1615 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1619 def is_intrasite_topology_disabled(self
):
1620 '''Returns True if intra-site topology is disabled for site'''
1621 return (self
.site_options
&
1622 dsdb
.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED
) != 0
1624 def is_intersite_topology_disabled(self
):
1625 '''Returns True if inter-site topology is disabled for site'''
1626 return ((self
.site_options
&
1627 dsdb
.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED
)
1630 def is_random_bridgehead_disabled(self
):
1631 '''Returns True if selection of random bridgehead is disabled'''
1632 return (self
.site_options
&
1633 dsdb
.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED
) != 0
1635 def is_detect_stale_disabled(self
):
1636 '''Returns True if detect stale is disabled for site'''
1637 return (self
.site_options
&
1638 dsdb
.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED
) != 0
1640 def is_cleanup_ntdsconn_disabled(self
):
1641 '''Returns True if NTDS Connection cleanup is disabled for site'''
1642 return (self
.site_options
&
1643 dsdb
.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED
) != 0
1645 def same_site(self
, dsa
):
1646 '''Return True if dsa is in this site'''
1647 if self
.get_dsa(dsa
.dsa_dnstr
):
1651 def is_rodc_site(self
):
1652 if len(self
.dsa_table
) > 0 and len(self
.rw_dsa_table
) == 0:
1657 '''Debug dump string output of class'''
1658 text
= "%s:" % self
.__class
__.__name
__
1659 text
= text
+ "\n\tdn=%s" % self
.site_dnstr
1660 text
= text
+ "\n\toptions=0x%X" % self
.site_options
1661 text
= text
+ "\n\ttopo_generator=%s" % self
.site_topo_generator
1662 text
= text
+ "\n\ttopo_failover=%d" % self
.site_topo_failover
1663 for key
, dsa
in self
.dsa_table
.items():
1664 text
= text
+ "\n%s" % dsa
1668 class GraphNode(object):
1669 """A graph node describing a set of edges that should be directed to it.
1671 Each edge is a connection for a particular naming context replica directed
1672 from another node in the forest to this node.
1675 def __init__(self
, dsa_dnstr
, max_node_edges
):
1676 """Instantiate the graph node according to a DSA dn string
1678 :param max_node_edges: maximum number of edges that should ever
1679 be directed to the node
1681 self
.max_edges
= max_node_edges
1682 self
.dsa_dnstr
= dsa_dnstr
1686 text
= "%s:" % self
.__class
__.__name
__
1687 text
= text
+ "\n\tdsa_dnstr=%s" % self
.dsa_dnstr
1688 text
= text
+ "\n\tmax_edges=%d" % self
.max_edges
1690 for i
, edge
in enumerate(self
.edge_from
):
1691 text
= text
+ "\n\tedge_from[%d]=%s" % (i
, edge
)
1694 def add_edge_from(self
, from_dsa_dnstr
):
1695 """Add an edge from the dsa to our graph nodes edge from list
1697 :param from_dsa_dnstr: the dsa that the edge emanates from
1699 assert from_dsa_dnstr
is not None
1701 # No edges from myself to myself
1702 if from_dsa_dnstr
== self
.dsa_dnstr
:
1704 # Only one edge from a particular node
1705 if from_dsa_dnstr
in self
.edge_from
:
1707 # Not too many edges
1708 if len(self
.edge_from
) >= self
.max_edges
:
1710 self
.edge_from
.append(from_dsa_dnstr
)
1713 def add_edges_from_connections(self
, dsa
):
1714 """For each nTDSConnection object associated with a particular
1715 DSA, we test if it implies an edge to this graph node (i.e.
1716 the "fromServer" attribute). If it does then we add an
1717 edge from the server unless we are over the max edges for this
1720 :param dsa: dsa with a dnstr equivalent to his graph node
1722 for connect
in dsa
.connect_table
.values():
1723 self
.add_edge_from(connect
.from_dnstr
)
1725 def add_connections_from_edges(self
, dsa
):
1726 """For each edge directed to this graph node, ensure there
1727 is a corresponding nTDSConnection object in the dsa.
1729 for edge_dnstr
in self
.edge_from
:
1730 connections
= dsa
.get_connection_by_from_dnstr(edge_dnstr
)
1732 # For each edge directed to the NC replica that
1733 # "should be present" on the local DC, the KCC determines
1734 # whether an object c exists such that:
1736 # c is a child of the DC's nTDSDSA object.
1737 # c.objectCategory = nTDSConnection
1739 # Given the NC replica ri from which the edge is directed,
1740 # c.fromServer is the dsname of the nTDSDSA object of
1741 # the DC on which ri "is present".
1743 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1746 for connect
in connections
:
1747 if connect
.is_rodc_topology():
1754 # if no such object exists then the KCC adds an object
1755 # c with the following attributes
1757 # Generate a new dnstr for this nTDSConnection
1758 opt
= dsdb
.NTDSCONN_OPT_IS_GENERATED
1759 flags
= (dsdb
.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1760 dsdb
.SYSTEM_FLAG_CONFIG_ALLOW_MOVE
)
1762 dsa
.new_connection(opt
, flags
, None, edge_dnstr
, None)
1764 def has_sufficient_edges(self
):
1765 '''Return True if we have met the maximum "from edges" criteria'''
1766 if len(self
.edge_from
) >= self
.max_edges
:
1771 class Transport(object):
1772 """Class defines a Inter-site transport found under Sites
1775 def __init__(self
, dnstr
):
1780 self
.address_attr
= None
1781 self
.bridgehead_list
= []
1784 '''Debug dump string output of Transport object'''
1786 text
= "%s:\n\tdn=%s" % (self
.__class
__.__name
__, self
.dnstr
)
1787 text
= text
+ "\n\tguid=%s" % str(self
.guid
)
1788 text
= text
+ "\n\toptions=%d" % self
.options
1789 text
= text
+ "\n\taddress_attr=%s" % self
.address_attr
1790 text
= text
+ "\n\tname=%s" % self
.name
1791 for dnstr
in self
.bridgehead_list
:
1792 text
= text
+ "\n\tbridgehead_list=%s" % dnstr
1796 def load_transport(self
, samdb
):
1797 """Given a Transport object with an prior initialization
1798 for the object's DN, search for the DN and load attributes
1801 attrs
= ["objectGUID",
1804 "bridgeheadServerListBL",
1805 "transportAddressAttribute"]
1807 res
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
,
1810 except ldb
.LdbError
, (enum
, estr
):
1811 raise Exception("Unable to find Transport for (%s) - (%s)" %
1815 self
.guid
= misc
.GUID(samdb
.schema_format_value("objectGUID",
1816 msg
["objectGUID"][0]))
1818 if "options" in msg
:
1819 self
.options
= int(msg
["options"][0])
1821 if "transportAddressAttribute" in msg
:
1822 self
.address_attr
= str(msg
["transportAddressAttribute"][0])
1825 self
.name
= str(msg
["name"][0])
1827 if "bridgeheadServerListBL" in msg
:
1828 for value
in msg
["bridgeheadServerListBL"]:
1829 dsdn
= dsdb_Dn(samdb
, value
)
1830 dnstr
= str(dsdn
.dn
)
1831 if dnstr
not in self
.bridgehead_list
:
1832 self
.bridgehead_list
.append(dnstr
)
1835 class RepsFromTo(object):
1836 """Class encapsulation of the NDR repsFromToBlob.
1838 Removes the necessity of external code having to
1839 understand about other_info or manipulation of
1842 def __init__(self
, nc_dnstr
=None, ndr_blob
=None):
1844 self
.__dict
__['to_be_deleted'] = False
1845 self
.__dict
__['nc_dnstr'] = nc_dnstr
1846 self
.__dict
__['update_flags'] = 0x0
1847 # XXX the following sounds dubious and/or better solved
1848 # elsewhere, but lets leave it for now. In particular, there
1849 # seems to be no reason for all the non-ndr generated
1850 # attributes to be handled in the round about way (e.g.
1851 # self.__dict__['to_be_deleted'] = False above). On the other
1852 # hand, it all seems to work. Hooray! Hands off!.
1856 # There is a very subtle bug here with python
1857 # and our NDR code. If you assign directly to
1858 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1859 # then a proper python GC reference count is not
1862 # To work around this we maintain an internal
1863 # reference to "dns_name(x)" and "other_info" elements
1864 # of repsFromToBlob. This internal reference
1865 # is hidden within this class but it is why you
1866 # see statements like this below:
1868 # self.__dict__['ndr_blob'].ctr.other_info = \
1869 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1871 # That would appear to be a redundant assignment but
1872 # it is necessary to hold a proper python GC reference
1874 if ndr_blob
is None:
1875 self
.__dict
__['ndr_blob'] = drsblobs
.repsFromToBlob()
1876 self
.__dict
__['ndr_blob'].version
= 0x1
1877 self
.__dict
__['dns_name1'] = None
1878 self
.__dict
__['dns_name2'] = None
1880 self
.__dict
__['ndr_blob'].ctr
.other_info
= \
1881 self
.__dict
__['other_info'] = drsblobs
.repsFromTo1OtherInfo()
1884 self
.__dict
__['ndr_blob'] = ndr_blob
1885 self
.__dict
__['other_info'] = ndr_blob
.ctr
.other_info
1887 if ndr_blob
.version
== 0x1:
1888 self
.__dict
__['dns_name1'] = ndr_blob
.ctr
.other_info
.dns_name
1889 self
.__dict
__['dns_name2'] = None
1891 self
.__dict
__['dns_name1'] = ndr_blob
.ctr
.other_info
.dns_name1
1892 self
.__dict
__['dns_name2'] = ndr_blob
.ctr
.other_info
.dns_name2
1895 '''Debug dump string output of class'''
1897 text
= "%s:" % self
.__class
__.__name
__
1898 text
+= "\n\tdnstr=%s" % self
.nc_dnstr
1899 text
+= "\n\tupdate_flags=0x%X" % self
.update_flags
1900 text
+= "\n\tversion=%d" % self
.version
1901 text
+= "\n\tsource_dsa_obj_guid=%s" % self
.source_dsa_obj_guid
1902 text
+= ("\n\tsource_dsa_invocation_id=%s" %
1903 self
.source_dsa_invocation_id
)
1904 text
+= "\n\ttransport_guid=%s" % self
.transport_guid
1905 text
+= "\n\treplica_flags=0x%X" % self
.replica_flags
1906 text
+= ("\n\tconsecutive_sync_failures=%d" %
1907 self
.consecutive_sync_failures
)
1908 text
+= "\n\tlast_success=%s" % self
.last_success
1909 text
+= "\n\tlast_attempt=%s" % self
.last_attempt
1910 text
+= "\n\tdns_name1=%s" % self
.dns_name1
1911 text
+= "\n\tdns_name2=%s" % self
.dns_name2
1912 text
+= "\n\tschedule[ "
1913 for slot
in self
.schedule
:
1914 text
+= "0x%X " % slot
1919 def __setattr__(self
, item
, value
):
1920 """Set an attribute and chyange update flag.
1922 Be aware that setting any RepsFromTo attribute will set the
1923 drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS update flag.
1925 if item
in ['schedule', 'replica_flags', 'transport_guid',
1926 'source_dsa_obj_guid', 'source_dsa_invocation_id',
1927 'consecutive_sync_failures', 'last_success',
1930 if item
in ['replica_flags']:
1931 self
.__dict
__['update_flags'] |
= drsuapi
.DRSUAPI_DRS_UPDATE_FLAGS
1932 elif item
in ['schedule']:
1933 self
.__dict
__['update_flags'] |
= drsuapi
.DRSUAPI_DRS_UPDATE_SCHEDULE
1935 setattr(self
.__dict
__['ndr_blob'].ctr
, item
, value
)
1937 elif item
in ['dns_name1']:
1938 self
.__dict
__['dns_name1'] = value
1940 if self
.__dict
__['ndr_blob'].version
== 0x1:
1941 self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name
= \
1942 self
.__dict
__['dns_name1']
1944 self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name1
= \
1945 self
.__dict
__['dns_name1']
1947 elif item
in ['dns_name2']:
1948 self
.__dict
__['dns_name2'] = value
1950 if self
.__dict
__['ndr_blob'].version
== 0x1:
1951 raise AttributeError(item
)
1953 self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name2
= \
1954 self
.__dict
__['dns_name2']
1956 elif item
in ['nc_dnstr']:
1957 self
.__dict
__['nc_dnstr'] = value
1959 elif item
in ['to_be_deleted']:
1960 self
.__dict
__['to_be_deleted'] = value
1962 elif item
in ['version']:
1963 raise AttributeError("Attempt to set readonly attribute %s" % item
)
1965 raise AttributeError("Unknown attribute %s" % item
)
1967 self
.__dict
__['update_flags'] |
= drsuapi
.DRSUAPI_DRS_UPDATE_ADDRESS
1969 def __getattr__(self
, item
):
1970 """Overload of RepsFromTo attribute retrieval.
1972 Allows external code to ignore substructures within the blob
1974 if item
in ['schedule', 'replica_flags', 'transport_guid',
1975 'source_dsa_obj_guid', 'source_dsa_invocation_id',
1976 'consecutive_sync_failures', 'last_success',
1978 return getattr(self
.__dict
__['ndr_blob'].ctr
, item
)
1980 elif item
in ['version']:
1981 return self
.__dict
__['ndr_blob'].version
1983 elif item
in ['dns_name1']:
1984 if self
.__dict
__['ndr_blob'].version
== 0x1:
1985 return self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name
1987 return self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name1
1989 elif item
in ['dns_name2']:
1990 if self
.__dict
__['ndr_blob'].version
== 0x1:
1991 raise AttributeError(item
)
1993 return self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name2
1995 elif item
in ['to_be_deleted']:
1996 return self
.__dict
__['to_be_deleted']
1998 elif item
in ['nc_dnstr']:
1999 return self
.__dict
__['nc_dnstr']
2001 elif item
in ['update_flags']:
2002 return self
.__dict
__['update_flags']
2004 raise AttributeError("Unknown attribute %s" % item
)
2006 def is_modified(self
):
2007 return (self
.update_flags
!= 0x0)
2009 def set_unmodified(self
):
2010 self
.__dict
__['update_flags'] = 0x0
2013 class SiteLink(object):
2014 """Class defines a site link found under sites
2017 def __init__(self
, dnstr
):
2020 self
.system_flags
= 0
2022 self
.schedule
= None
2023 self
.interval
= None
2027 '''Debug dump string output of Transport object'''
2029 text
= "%s:\n\tdn=%s" % (self
.__class
__.__name
__, self
.dnstr
)
2030 text
= text
+ "\n\toptions=%d" % self
.options
2031 text
= text
+ "\n\tsystem_flags=%d" % self
.system_flags
2032 text
= text
+ "\n\tcost=%d" % self
.cost
2033 text
= text
+ "\n\tinterval=%s" % self
.interval
2035 if self
.schedule
is not None:
2036 text
+= "\n\tschedule.size=%s" % self
.schedule
.size
2037 text
+= "\n\tschedule.bandwidth=%s" % self
.schedule
.bandwidth
2038 text
+= ("\n\tschedule.numberOfSchedules=%s" %
2039 self
.schedule
.numberOfSchedules
)
2041 for i
, header
in enumerate(self
.schedule
.headerArray
):
2042 text
+= ("\n\tschedule.headerArray[%d].type=%d" %
2044 text
+= ("\n\tschedule.headerArray[%d].offset=%d" %
2046 text
= text
+ "\n\tschedule.dataArray[%d].slots[ " % i
2047 for slot
in self
.schedule
.dataArray
[i
].slots
:
2048 text
= text
+ "0x%X " % slot
2051 for dnstr
in self
.site_list
:
2052 text
= text
+ "\n\tsite_list=%s" % dnstr
2055 def load_sitelink(self
, samdb
):
2056 """Given a siteLink object with an prior initialization
2057 for the object's DN, search for the DN and load attributes
2067 res
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
,
2068 attrs
=attrs
, controls
=['extended_dn:0'])
2070 except ldb
.LdbError
, (enum
, estr
):
2071 raise Exception("Unable to find SiteLink for (%s) - (%s)" %
2076 if "options" in msg
:
2077 self
.options
= int(msg
["options"][0])
2079 if "systemFlags" in msg
:
2080 self
.system_flags
= int(msg
["systemFlags"][0])
2083 self
.cost
= int(msg
["cost"][0])
2085 if "replInterval" in msg
:
2086 self
.interval
= int(msg
["replInterval"][0])
2088 if "siteList" in msg
:
2089 for value
in msg
["siteList"]:
2090 dsdn
= dsdb_Dn(samdb
, value
)
2091 guid
= misc
.GUID(dsdn
.dn
.get_extended_component('GUID'))
2092 if guid
not in self
.site_list
:
2093 self
.site_list
.append(guid
)
2095 if "schedule" in msg
:
2096 self
.schedule
= ndr_unpack(drsblobs
.schedule
, value
)
2098 self
.schedule
= new_connection_schedule()
2101 class KCCFailedObject(object):
2102 def __init__(self
, uuid
, failure_count
, time_first_failure
,
2103 last_result
, dns_name
):
2105 self
.failure_count
= failure_count
2106 self
.time_first_failure
= time_first_failure
2107 self
.last_result
= last_result
2108 self
.dns_name
= dns_name
2111 ##################################################
2112 # Global Functions and Variables
2113 ##################################################
2115 def get_dsa_config_rep(dsa
):
2116 # Find configuration NC replica for the DSA
2117 for c_rep
in dsa
.current_rep_table
.values():
2118 if c_rep
.is_config():
2121 raise KCCError("Unable to find config NC replica for (%s)" %
2125 def sort_dsa_by_guid(dsa1
, dsa2
):
2126 "use ndr_pack for GUID comparison, as appears correct in some places"""
2127 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
2130 def new_connection_schedule():
2131 """Create a default schedule for an NTDSConnection or Sitelink. This
2132 is packed differently from the repltimes schedule used elsewhere
2133 in KCC (where the 168 nibbles are packed into 84 bytes).
2135 # 168 byte instances of the 0x01 value. The low order 4 bits
2136 # of the byte equate to 15 minute intervals within a single hour.
2137 # There are 168 bytes because there are 168 hours in a full week
2138 # Effectively we are saying to perform replication at the end of
2139 # each hour of the week
2140 schedule = drsblobs.schedule()
2143 schedule.bandwidth = 0
2144 schedule.numberOfSchedules = 1
2146 header = drsblobs.scheduleHeader()
2150 schedule.headerArray = [header]
2152 data = drsblobs.scheduleSlots()
2153 data.slots = [0x01] * 168
2155 schedule.dataArray = [data]