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
= {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 #XXX msg is never used
1039 msg
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
)
1042 except ldb
.LdbError
, (enum
, estr
):
1043 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
1046 raise Exception("Unable to search for (%s) - (%s)" %
1049 raise Exception("nTDSConnection for (%s) doesn't exist!" %
1057 # Prepare a message for modifying the samdb
1059 m
.dn
= ldb
.Dn(samdb
, self
.dnstr
)
1061 m
["enabledConnection"] = \
1062 ldb
.MessageElement(enablestr
, ldb
.FLAG_MOD_REPLACE
,
1063 "enabledConnection")
1065 ldb
.MessageElement(self
.from_dnstr
, ldb
.FLAG_MOD_REPLACE
,
1068 ldb
.MessageElement(str(self
.options
), ldb
.FLAG_MOD_REPLACE
,
1070 m
["systemFlags"] = \
1071 ldb
.MessageElement(str(self
.system_flags
), ldb
.FLAG_MOD_REPLACE
,
1074 if self
.transport_dnstr
is not None:
1075 m
["transportType"] = \
1076 ldb
.MessageElement(str(self
.transport_dnstr
),
1077 ldb
.FLAG_MOD_REPLACE
, "transportType")
1079 m
["transportType"] = \
1080 ldb
.MessageElement([], ldb
.FLAG_MOD_DELETE
, "transportType")
1082 if self
.schedule
is not None:
1084 ldb
.MessageElement(ndr_pack(self
.schedule
),
1085 ldb
.FLAG_MOD_REPLACE
, "schedule")
1088 ldb
.MessageElement([], ldb
.FLAG_MOD_DELETE
, "schedule")
1091 except ldb
.LdbError
, (enum
, estr
):
1092 raise Exception("Could not modify nTDSConnection for (%s) - (%s)" %
1095 def set_modified(self
, truefalse
):
1096 self
.to_be_modified
= truefalse
1098 def set_added(self
, truefalse
):
1099 self
.to_be_added
= truefalse
1101 def set_deleted(self
, truefalse
):
1102 self
.to_be_deleted
= truefalse
1104 def is_schedule_minimum_once_per_week(self
):
1105 """Returns True if our schedule includes at least one
1106 replication interval within the week. False otherwise
1108 # replinfo schedule is None means "always", while
1109 # NTDSConnection schedule is None means "never".
1110 if self
.schedule
is None or self
.schedule
.dataArray
[0] is None:
1113 for slot
in self
.schedule
.dataArray
[0].slots
:
1114 if (slot
& 0x0F) != 0x0:
1118 def is_equivalent_schedule(self
, sched
):
1119 """Returns True if our schedule is equivalent to the input
1120 comparison schedule.
1122 :param shed: schedule to compare to
1124 if self
.schedule
is not None:
1130 if ((self
.schedule
.size
!= sched
.size
or
1131 self
.schedule
.bandwidth
!= sched
.bandwidth
or
1132 self
.schedule
.numberOfSchedules
!= sched
.numberOfSchedules
)):
1135 for i
, header
in enumerate(self
.schedule
.headerArray
):
1137 if self
.schedule
.headerArray
[i
].type != sched
.headerArray
[i
].type:
1140 if self
.schedule
.headerArray
[i
].offset
!= \
1141 sched
.headerArray
[i
].offset
:
1144 for a
, b
in zip(self
.schedule
.dataArray
[i
].slots
,
1145 sched
.dataArray
[i
].slots
):
1150 def is_rodc_topology(self
):
1151 """Returns True if NTDS Connection specifies RODC
1154 if self
.options
& dsdb
.NTDSCONN_OPT_RODC_TOPOLOGY
== 0:
1158 def is_generated(self
):
1159 """Returns True if NTDS Connection was generated by the
1160 KCC topology algorithm as opposed to set by the administrator
1162 if self
.options
& dsdb
.NTDSCONN_OPT_IS_GENERATED
== 0:
1166 def is_override_notify_default(self
):
1167 """Returns True if NTDS Connection should override notify default
1169 if self
.options
& dsdb
.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT
== 0:
1173 def is_use_notify(self
):
1174 """Returns True if NTDS Connection should use notify
1176 if self
.options
& dsdb
.NTDSCONN_OPT_USE_NOTIFY
== 0:
1180 def is_twoway_sync(self
):
1181 """Returns True if NTDS Connection should use twoway sync
1183 if self
.options
& dsdb
.NTDSCONN_OPT_TWOWAY_SYNC
== 0:
1187 def is_intersite_compression_disabled(self
):
1188 """Returns True if NTDS Connection intersite compression
1191 if self
.options
& dsdb
.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
== 0:
1195 def is_user_owned_schedule(self
):
1196 """Returns True if NTDS Connection has a user owned schedule
1198 if self
.options
& dsdb
.NTDSCONN_OPT_USER_OWNED_SCHEDULE
== 0:
1202 def is_enabled(self
):
1203 """Returns True if NTDS Connection is enabled
1207 def get_from_dnstr(self
):
1208 '''Return fromServer dn string attribute'''
1209 return self
.from_dnstr
1212 class Partition(NamingContext
):
1213 """A naming context discovered thru Partitions DN of the config schema.
1215 This is a more specific form of NamingContext class (inheriting from that
1216 class) and it identifies unique attributes enumerated in the Partitions
1217 such as which nTDSDSAs are cross referenced for replicas
1219 def __init__(self
, partstr
):
1220 self
.partstr
= partstr
1222 self
.system_flags
= 0
1223 self
.rw_location_list
= []
1224 self
.ro_location_list
= []
1226 # We don't have enough info to properly
1227 # fill in the naming context yet. We'll get that
1228 # fully set up with load_partition().
1229 NamingContext
.__init
__(self
, None)
1231 def load_partition(self
, samdb
):
1232 """Given a Partition class object that has been initialized with its
1233 partition dn string, load the partition from the sam database, identify
1234 the type of the partition (schema, domain, etc) and record the list of
1235 nTDSDSAs that appear in the cross reference attributes
1236 msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations.
1238 :param samdb: sam database to load partition from
1243 "msDS-NC-Replica-Locations",
1244 "msDS-NC-RO-Replica-Locations"]
1246 res
= samdb
.search(base
=self
.partstr
, scope
=ldb
.SCOPE_BASE
,
1249 except ldb
.LdbError
, (enum
, estr
):
1250 raise Exception("Unable to find partition for (%s) - (%s)" % (
1251 self
.partstr
, estr
))
1254 for k
in msg
.keys():
1259 if msg
[k
][0].upper().lstrip().rstrip() == "TRUE":
1262 self
.enabled
= False
1265 if k
== "systemFlags":
1266 self
.system_flags
= int(msg
[k
][0])
1269 for value
in msg
[k
]:
1270 dsdn
= dsdb_Dn(samdb
, value
)
1271 dnstr
= str(dsdn
.dn
)
1274 self
.nc_dnstr
= dnstr
1277 if k
== "msDS-NC-Replica-Locations":
1278 self
.rw_location_list
.append(dnstr
)
1281 if k
== "msDS-NC-RO-Replica-Locations":
1282 self
.ro_location_list
.append(dnstr
)
1285 # Now identify what type of NC this partition
1287 self
.identify_by_basedn(samdb
)
1289 def is_enabled(self
):
1290 """Returns True if partition is enabled
1292 return self
.is_enabled
1294 def is_foreign(self
):
1295 """Returns True if this is not an Active Directory NC in our
1296 forest but is instead something else (e.g. a foreign NC)
1298 if (self
.system_flags
& dsdb
.SYSTEM_FLAG_CR_NTDS_NC
) == 0:
1303 def should_be_present(self
, target_dsa
):
1304 """Tests whether this partition should have an NC replica
1305 on the target dsa. This method returns a tuple of
1306 needed=True/False, ro=True/False, partial=True/False
1308 :param target_dsa: should NC be present on target dsa
1313 # If this is the config, schema, or default
1314 # domain NC for the target dsa then it should
1316 needed
= (self
.nc_type
== NCType
.config
or
1317 self
.nc_type
== NCType
.schema
or
1318 (self
.nc_type
== NCType
.domain
and
1319 self
.nc_dnstr
== target_dsa
.default_dnstr
))
1321 # A writable replica of an application NC should be present
1322 # if there a cross reference to the target DSA exists. Depending
1323 # on whether the DSA is ro we examine which type of cross reference
1324 # to look for (msDS-NC-Replica-Locations or
1325 # msDS-NC-RO-Replica-Locations
1326 if self
.nc_type
== NCType
.application
:
1327 if target_dsa
.is_ro():
1328 if target_dsa
.dsa_dnstr
in self
.ro_location_list
:
1331 if target_dsa
.dsa_dnstr
in self
.rw_location_list
:
1334 # If the target dsa is a gc then a partial replica of a
1335 # domain NC (other than the DSAs default domain) should exist
1336 # if there is also a cross reference for the DSA
1337 if (target_dsa
.is_gc() and
1338 self
.nc_type
== NCType
.domain
and
1339 self
.nc_dnstr
!= target_dsa
.default_dnstr
and
1340 (target_dsa
.dsa_dnstr
in self
.ro_location_list
or
1341 target_dsa
.dsa_dnstr
in self
.rw_location_list
)):
1345 # partial NCs are always readonly
1346 if needed
and (target_dsa
.is_ro() or partial
):
1349 return needed
, ro
, partial
1352 '''Debug dump string output of class'''
1353 text
= "%s" % NamingContext
.__str
__(self
)
1354 text
= text
+ "\n\tpartdn=%s" % self
.partstr
1355 for k
in self
.rw_location_list
:
1356 text
= text
+ "\n\tmsDS-NC-Replica-Locations=%s" % k
1357 for k
in self
.ro_location_list
:
1358 text
= text
+ "\n\tmsDS-NC-RO-Replica-Locations=%s" % k
1363 """An individual site object discovered thru the configuration
1364 naming context. Contains all DSAs that exist within the site
1366 def __init__(self
, site_dnstr
, nt_now
):
1367 self
.site_dnstr
= site_dnstr
1368 self
.site_guid
= None
1369 self
.site_options
= 0
1370 self
.site_topo_generator
= None
1371 self
.site_topo_failover
= 0 # appears to be in minutes
1373 self
.rw_dsa_table
= {}
1374 self
.nt_now
= nt_now
1376 def load_site(self
, samdb
):
1377 """Loads the NTDS Site Settions options attribute for the site
1378 as well as querying and loading all DSAs that appear within
1381 ssdn
= "CN=NTDS Site Settings,%s" % self
.site_dnstr
1383 "interSiteTopologyFailover",
1384 "interSiteTopologyGenerator"]
1386 res
= samdb
.search(base
=ssdn
, scope
=ldb
.SCOPE_BASE
,
1388 self_res
= samdb
.search(base
=self
.site_dnstr
, scope
=ldb
.SCOPE_BASE
,
1389 attrs
=['objectGUID'])
1390 except ldb
.LdbError
, (enum
, estr
):
1391 raise Exception("Unable to find site settings for (%s) - (%s)" %
1395 if "options" in msg
:
1396 self
.site_options
= int(msg
["options"][0])
1398 if "interSiteTopologyGenerator" in msg
:
1399 self
.site_topo_generator
= \
1400 str(msg
["interSiteTopologyGenerator"][0])
1402 if "interSiteTopologyFailover" in msg
:
1403 self
.site_topo_failover
= int(msg
["interSiteTopologyFailover"][0])
1406 if "objectGUID" in msg
:
1407 self
.site_guid
= misc
.GUID(samdb
.schema_format_value("objectGUID",
1408 msg
["objectGUID"][0]))
1410 self
.load_all_dsa(samdb
)
1412 def load_all_dsa(self
, samdb
):
1413 """Discover all nTDSDSA thru the sites entry and
1414 instantiate and load the DSAs. Each dsa is inserted
1415 into the dsa_table by dn string.
1418 res
= samdb
.search(self
.site_dnstr
,
1419 scope
=ldb
.SCOPE_SUBTREE
,
1420 expression
="(objectClass=nTDSDSA)")
1421 except ldb
.LdbError
, (enum
, estr
):
1422 raise Exception("Unable to find nTDSDSAs - (%s)" % estr
)
1428 if dnstr
in self
.dsa_table
:
1431 dsa
= DirectoryServiceAgent(dnstr
)
1435 # Assign this dsa to my dsa table
1436 # and index by dsa dn
1437 self
.dsa_table
[dnstr
] = dsa
1439 self
.rw_dsa_table
[dnstr
] = dsa
1441 def get_dsa_by_guidstr(self
, guidstr
): # XXX unused
1442 for dsa
in self
.dsa_table
.values():
1443 if str(dsa
.dsa_guid
) == guidstr
:
1447 def get_dsa(self
, dnstr
):
1448 """Return a previously loaded DSA object by consulting
1449 the sites dsa_table for the provided DSA dn string
1451 :return: None if DSA doesn't exist
1453 return self
.dsa_table
.get(dnstr
)
1455 def select_istg(self
, samdb
, mydsa
, ro
):
1456 """Determine if my DC should be an intersite topology
1457 generator. If my DC is the istg and is both a writeable
1458 DC and the database is opened in write mode then we perform
1459 an originating update to set the interSiteTopologyGenerator
1460 attribute in the NTDS Site Settings object. An RODC always
1461 acts as an ISTG for itself.
1463 # The KCC on an RODC always acts as an ISTG for itself
1465 mydsa
.dsa_is_istg
= True
1466 self
.site_topo_generator
= mydsa
.dsa_dnstr
1469 c_rep
= get_dsa_config_rep(mydsa
)
1471 # Load repsFrom and replUpToDateVector if not already loaded
1472 # so we can get the current state of the config replica and
1473 # whether we are getting updates from the istg
1474 c_rep
.load_repsFrom(samdb
)
1476 c_rep
.load_replUpToDateVector(samdb
)
1478 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1479 # First, the KCC on a writable DC determines whether it acts
1480 # as an ISTG for its site
1482 # Let s be the object such that s!lDAPDisplayName = nTDSDSA
1483 # and classSchema in s!objectClass.
1485 # Let D be the sequence of objects o in the site of the local
1486 # DC such that o!objectCategory = s. D is sorted in ascending
1487 # order by objectGUID.
1489 # Which is a fancy way of saying "sort all the nTDSDSA objects
1490 # in the site by guid in ascending order". Place sorted list
1492 D_sort
= sorted(self
.rw_dsa_table
.values(), cmp=sort_dsa_by_guid
)
1494 # double word number of 100 nanosecond intervals since 1600s
1496 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1497 # if o!interSiteTopologyFailover is 0 or has no value.
1499 # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1500 # so it appears we have to turn f into the same interval
1502 # interSiteTopologyFailover (if set) appears to be in minutes
1503 # so we'll need to convert to senconds and then 100 nanosecond
1505 # XXX [MS-ADTS] 6.2.2.3.1 says it is seconds, not minutes.
1507 # 10,000,000 is number of 100 nanosecond intervals in a second
1508 if self
.site_topo_failover
== 0:
1509 f
= 2 * 60 * 60 * 10000000
1511 f
= self
.site_topo_failover
* 60 * 10000000
1513 # Let o be the site settings object for the site of the local
1514 # DC, or NULL if no such o exists.
1515 d_dsa
= self
.dsa_table
.get(self
.site_topo_generator
)
1517 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1518 # If o != NULL and o!interSiteTopologyGenerator is not the
1519 # nTDSDSA object for the local DC and
1520 # o!interSiteTopologyGenerator is an element dj of sequence D:
1522 if d_dsa
is not None and d_dsa
is not mydsa
:
1523 # From MS-ADTS 6.2.2.3.1 ISTG Selection:
1524 # Let c be the cursor in the replUpToDateVector variable
1525 # associated with the NC replica of the config NC such
1526 # that c.uuidDsa = dj!invocationId. If no such c exists
1527 # (No evidence of replication from current ITSG):
1531 # Else if the current time < c.timeLastSyncSuccess - f
1532 # (Evidence of time sync problem on current ISTG):
1536 # Else (Evidence of replication from current ITSG):
1538 # Let t = c.timeLastSyncSuccess.
1540 # last_success appears to be a double word containing
1541 # number of 100 nanosecond intervals since the 1600s
1542 j_idx
= D_sort
.index(d_dsa
)
1545 for cursor
in c_rep
.rep_replUpToDateVector_cursors
:
1546 if d_dsa
.dsa_ivid
== cursor
.source_dsa_invocation_id
:
1554 #XXX doc says current time < c.timeLastSyncSuccess - f
1555 # which is true only if f is negative or clocks are wrong.
1556 # f is not negative in the default case (2 hours).
1557 elif self
.nt_now
- cursor
.last_sync_success
> f
:
1562 t_time
= cursor
.last_sync_success
1564 # Otherwise (Nominate local DC as ISTG):
1565 # Let i be the integer such that di is the nTDSDSA
1566 # object for the local DC.
1567 # Let t = the current time.
1569 i_idx
= D_sort
.index(mydsa
)
1570 t_time
= self
.nt_now
1572 # Compute a function that maintains the current ISTG if
1573 # it is alive, cycles through other candidates if not.
1575 # Let k be the integer (i + ((current time - t) /
1576 # o!interSiteTopologyFailover)) MOD |D|.
1578 # Note: We don't want to divide by zero here so they must
1579 # have meant "f" instead of "o!interSiteTopologyFailover"
1580 k_idx
= (i_idx
+ ((self
.nt_now
- t_time
) / f
)) % len(D_sort
)
1582 # The local writable DC acts as an ISTG for its site if and
1583 # only if dk is the nTDSDSA object for the local DC. If the
1584 # local DC does not act as an ISTG, the KCC skips the
1585 # remainder of this task.
1586 d_dsa
= D_sort
[k_idx
]
1587 d_dsa
.dsa_is_istg
= True
1589 # Update if we are the ISTG, otherwise return
1590 if d_dsa
is not mydsa
:
1594 if self
.site_topo_generator
== mydsa
.dsa_dnstr
:
1597 self
.site_topo_generator
= mydsa
.dsa_dnstr
1599 # If readonly database then do not perform a
1604 # Perform update to the samdb
1605 ssdn
= "CN=NTDS Site Settings,%s" % self
.site_dnstr
1608 m
.dn
= ldb
.Dn(samdb
, ssdn
)
1610 m
["interSiteTopologyGenerator"] = \
1611 ldb
.MessageElement(mydsa
.dsa_dnstr
, ldb
.FLAG_MOD_REPLACE
,
1612 "interSiteTopologyGenerator")
1616 except ldb
.LdbError
, estr
:
1618 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1622 def is_intrasite_topology_disabled(self
):
1623 '''Returns True if intra-site topology is disabled for site'''
1624 return (self
.site_options
&
1625 dsdb
.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED
) != 0
1627 def is_intersite_topology_disabled(self
):
1628 '''Returns True if inter-site topology is disabled for site'''
1629 return ((self
.site_options
&
1630 dsdb
.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED
)
1633 def is_random_bridgehead_disabled(self
):
1634 '''Returns True if selection of random bridgehead is disabled'''
1635 return (self
.site_options
&
1636 dsdb
.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED
) != 0
1638 def is_detect_stale_disabled(self
):
1639 '''Returns True if detect stale is disabled for site'''
1640 return (self
.site_options
&
1641 dsdb
.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED
) != 0
1643 def is_cleanup_ntdsconn_disabled(self
):
1644 '''Returns True if NTDS Connection cleanup is disabled for site'''
1645 return (self
.site_options
&
1646 dsdb
.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED
) != 0
1648 def same_site(self
, dsa
):
1649 '''Return True if dsa is in this site'''
1650 if self
.get_dsa(dsa
.dsa_dnstr
):
1654 def is_rodc_site(self
):
1655 if len(self
.dsa_table
) > 0 and len(self
.rw_dsa_table
) == 0:
1660 '''Debug dump string output of class'''
1661 text
= "%s:" % self
.__class
__.__name
__
1662 text
= text
+ "\n\tdn=%s" % self
.site_dnstr
1663 text
= text
+ "\n\toptions=0x%X" % self
.site_options
1664 text
= text
+ "\n\ttopo_generator=%s" % self
.site_topo_generator
1665 text
= text
+ "\n\ttopo_failover=%d" % self
.site_topo_failover
1666 for key
, dsa
in self
.dsa_table
.items():
1667 text
= text
+ "\n%s" % dsa
1671 class GraphNode(object):
1672 """A graph node describing a set of edges that should be directed to it.
1674 Each edge is a connection for a particular naming context replica directed
1675 from another node in the forest to this node.
1678 def __init__(self
, dsa_dnstr
, max_node_edges
):
1679 """Instantiate the graph node according to a DSA dn string
1681 :param max_node_edges: maximum number of edges that should ever
1682 be directed to the node
1684 self
.max_edges
= max_node_edges
1685 self
.dsa_dnstr
= dsa_dnstr
1689 text
= "%s:" % self
.__class
__.__name
__
1690 text
= text
+ "\n\tdsa_dnstr=%s" % self
.dsa_dnstr
1691 text
= text
+ "\n\tmax_edges=%d" % self
.max_edges
1693 for i
, edge
in enumerate(self
.edge_from
):
1694 text
= text
+ "\n\tedge_from[%d]=%s" % (i
, edge
)
1697 def add_edge_from(self
, from_dsa_dnstr
):
1698 """Add an edge from the dsa to our graph nodes edge from list
1700 :param from_dsa_dnstr: the dsa that the edge emanates from
1702 assert from_dsa_dnstr
is not None
1704 # No edges from myself to myself
1705 if from_dsa_dnstr
== self
.dsa_dnstr
:
1707 # Only one edge from a particular node
1708 if from_dsa_dnstr
in self
.edge_from
:
1710 # Not too many edges
1711 if len(self
.edge_from
) >= self
.max_edges
:
1713 self
.edge_from
.append(from_dsa_dnstr
)
1716 def add_edges_from_connections(self
, dsa
):
1717 """For each nTDSConnection object associated with a particular
1718 DSA, we test if it implies an edge to this graph node (i.e.
1719 the "fromServer" attribute). If it does then we add an
1720 edge from the server unless we are over the max edges for this
1723 :param dsa: dsa with a dnstr equivalent to his graph node
1725 for connect
in dsa
.connect_table
.values():
1726 self
.add_edge_from(connect
.from_dnstr
)
1728 def add_connections_from_edges(self
, dsa
):
1729 """For each edge directed to this graph node, ensure there
1730 is a corresponding nTDSConnection object in the dsa.
1732 for edge_dnstr
in self
.edge_from
:
1733 connections
= dsa
.get_connection_by_from_dnstr(edge_dnstr
)
1735 # For each edge directed to the NC replica that
1736 # "should be present" on the local DC, the KCC determines
1737 # whether an object c exists such that:
1739 # c is a child of the DC's nTDSDSA object.
1740 # c.objectCategory = nTDSConnection
1742 # Given the NC replica ri from which the edge is directed,
1743 # c.fromServer is the dsname of the nTDSDSA object of
1744 # the DC on which ri "is present".
1746 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1749 for connect
in connections
:
1750 if connect
.is_rodc_topology():
1757 # if no such object exists then the KCC adds an object
1758 # c with the following attributes
1760 # Generate a new dnstr for this nTDSConnection
1761 opt
= dsdb
.NTDSCONN_OPT_IS_GENERATED
1762 flags
= (dsdb
.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1763 dsdb
.SYSTEM_FLAG_CONFIG_ALLOW_MOVE
)
1765 dsa
.new_connection(opt
, flags
, None, edge_dnstr
, None)
1767 def has_sufficient_edges(self
):
1768 '''Return True if we have met the maximum "from edges" criteria'''
1769 if len(self
.edge_from
) >= self
.max_edges
:
1774 class Transport(object):
1775 """Class defines a Inter-site transport found under Sites
1778 def __init__(self
, dnstr
):
1783 self
.address_attr
= None
1784 self
.bridgehead_list
= []
1787 '''Debug dump string output of Transport object'''
1789 text
= "%s:\n\tdn=%s" % (self
.__class
__.__name
__, self
.dnstr
)
1790 text
= text
+ "\n\tguid=%s" % str(self
.guid
)
1791 text
= text
+ "\n\toptions=%d" % self
.options
1792 text
= text
+ "\n\taddress_attr=%s" % self
.address_attr
1793 text
= text
+ "\n\tname=%s" % self
.name
1794 for dnstr
in self
.bridgehead_list
:
1795 text
= text
+ "\n\tbridgehead_list=%s" % dnstr
1799 def load_transport(self
, samdb
):
1800 """Given a Transport object with an prior initialization
1801 for the object's DN, search for the DN and load attributes
1804 attrs
= ["objectGUID",
1807 "bridgeheadServerListBL",
1808 "transportAddressAttribute"]
1810 res
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
,
1813 except ldb
.LdbError
, (enum
, estr
):
1814 raise Exception("Unable to find Transport for (%s) - (%s)" %
1818 self
.guid
= misc
.GUID(samdb
.schema_format_value("objectGUID",
1819 msg
["objectGUID"][0]))
1821 if "options" in msg
:
1822 self
.options
= int(msg
["options"][0])
1824 if "transportAddressAttribute" in msg
:
1825 self
.address_attr
= str(msg
["transportAddressAttribute"][0])
1828 self
.name
= str(msg
["name"][0])
1830 if "bridgeheadServerListBL" in msg
:
1831 for value
in msg
["bridgeheadServerListBL"]:
1832 dsdn
= dsdb_Dn(samdb
, value
)
1833 dnstr
= str(dsdn
.dn
)
1834 if dnstr
not in self
.bridgehead_list
:
1835 self
.bridgehead_list
.append(dnstr
)
1838 class RepsFromTo(object):
1839 """Class encapsulation of the NDR repsFromToBlob.
1841 Removes the necessity of external code having to
1842 understand about other_info or manipulation of
1845 def __init__(self
, nc_dnstr
=None, ndr_blob
=None):
1847 self
.__dict
__['to_be_deleted'] = False
1848 self
.__dict
__['nc_dnstr'] = nc_dnstr
1849 self
.__dict
__['update_flags'] = 0x0
1850 # XXX the following sounds dubious and/or better solved
1851 # elsewhere, but lets leave it for now. In particular, there
1852 # seems to be no reason for all the non-ndr generated
1853 # attributes to be handled in the round about way (e.g.
1854 # self.__dict__['to_be_deleted'] = False above). On the other
1855 # hand, it all seems to work. Hooray! Hands off!.
1859 # There is a very subtle bug here with python
1860 # and our NDR code. If you assign directly to
1861 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1862 # then a proper python GC reference count is not
1865 # To work around this we maintain an internal
1866 # reference to "dns_name(x)" and "other_info" elements
1867 # of repsFromToBlob. This internal reference
1868 # is hidden within this class but it is why you
1869 # see statements like this below:
1871 # self.__dict__['ndr_blob'].ctr.other_info = \
1872 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1874 # That would appear to be a redundant assignment but
1875 # it is necessary to hold a proper python GC reference
1877 if ndr_blob
is None:
1878 self
.__dict
__['ndr_blob'] = drsblobs
.repsFromToBlob()
1879 self
.__dict
__['ndr_blob'].version
= 0x1
1880 self
.__dict
__['dns_name1'] = None
1881 self
.__dict
__['dns_name2'] = None
1883 self
.__dict
__['ndr_blob'].ctr
.other_info
= \
1884 self
.__dict
__['other_info'] = drsblobs
.repsFromTo1OtherInfo()
1887 self
.__dict
__['ndr_blob'] = ndr_blob
1888 self
.__dict
__['other_info'] = ndr_blob
.ctr
.other_info
1890 if ndr_blob
.version
== 0x1:
1891 self
.__dict
__['dns_name1'] = ndr_blob
.ctr
.other_info
.dns_name
1892 self
.__dict
__['dns_name2'] = None
1894 self
.__dict
__['dns_name1'] = ndr_blob
.ctr
.other_info
.dns_name1
1895 self
.__dict
__['dns_name2'] = ndr_blob
.ctr
.other_info
.dns_name2
1898 '''Debug dump string output of class'''
1900 text
= "%s:" % self
.__class
__.__name
__
1901 text
+= "\n\tdnstr=%s" % self
.nc_dnstr
1902 text
+= "\n\tupdate_flags=0x%X" % self
.update_flags
1903 text
+= "\n\tversion=%d" % self
.version
1904 text
+= "\n\tsource_dsa_obj_guid=%s" % self
.source_dsa_obj_guid
1905 text
+= ("\n\tsource_dsa_invocation_id=%s" %
1906 self
.source_dsa_invocation_id
)
1907 text
+= "\n\ttransport_guid=%s" % self
.transport_guid
1908 text
+= "\n\treplica_flags=0x%X" % self
.replica_flags
1909 text
+= ("\n\tconsecutive_sync_failures=%d" %
1910 self
.consecutive_sync_failures
)
1911 text
+= "\n\tlast_success=%s" % self
.last_success
1912 text
+= "\n\tlast_attempt=%s" % self
.last_attempt
1913 text
+= "\n\tdns_name1=%s" % self
.dns_name1
1914 text
+= "\n\tdns_name2=%s" % self
.dns_name2
1915 text
+= "\n\tschedule[ "
1916 for slot
in self
.schedule
:
1917 text
+= "0x%X " % slot
1922 def __setattr__(self
, item
, value
):
1923 """Set an attribute and chyange update flag.
1925 Be aware that setting any RepsFromTo attribute will set the
1926 drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS update flag.
1928 if item
in ['schedule', 'replica_flags', 'transport_guid',
1929 'source_dsa_obj_guid', 'source_dsa_invocation_id',
1930 'consecutive_sync_failures', 'last_success',
1933 if item
in ['replica_flags']:
1934 self
.__dict
__['update_flags'] |
= drsuapi
.DRSUAPI_DRS_UPDATE_FLAGS
1935 elif item
in ['schedule']:
1936 self
.__dict
__['update_flags'] |
= drsuapi
.DRSUAPI_DRS_UPDATE_SCHEDULE
1938 setattr(self
.__dict
__['ndr_blob'].ctr
, item
, value
)
1940 elif item
in ['dns_name1']:
1941 self
.__dict
__['dns_name1'] = value
1943 if self
.__dict
__['ndr_blob'].version
== 0x1:
1944 self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name
= \
1945 self
.__dict
__['dns_name1']
1947 self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name1
= \
1948 self
.__dict
__['dns_name1']
1950 elif item
in ['dns_name2']:
1951 self
.__dict
__['dns_name2'] = value
1953 if self
.__dict
__['ndr_blob'].version
== 0x1:
1954 raise AttributeError(item
)
1956 self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name2
= \
1957 self
.__dict
__['dns_name2']
1959 elif item
in ['nc_dnstr']:
1960 self
.__dict
__['nc_dnstr'] = value
1962 elif item
in ['to_be_deleted']:
1963 self
.__dict
__['to_be_deleted'] = value
1965 elif item
in ['version']:
1966 raise AttributeError("Attempt to set readonly attribute %s" % item
)
1968 raise AttributeError("Unknown attribute %s" % item
)
1970 self
.__dict
__['update_flags'] |
= drsuapi
.DRSUAPI_DRS_UPDATE_ADDRESS
1972 def __getattr__(self
, item
):
1973 """Overload of RepsFromTo attribute retrieval.
1975 Allows external code to ignore substructures within the blob
1977 if item
in ['schedule', 'replica_flags', 'transport_guid',
1978 'source_dsa_obj_guid', 'source_dsa_invocation_id',
1979 'consecutive_sync_failures', 'last_success',
1981 return getattr(self
.__dict
__['ndr_blob'].ctr
, item
)
1983 elif item
in ['version']:
1984 return self
.__dict
__['ndr_blob'].version
1986 elif item
in ['dns_name1']:
1987 if self
.__dict
__['ndr_blob'].version
== 0x1:
1988 return self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name
1990 return self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name1
1992 elif item
in ['dns_name2']:
1993 if self
.__dict
__['ndr_blob'].version
== 0x1:
1994 raise AttributeError(item
)
1996 return self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name2
1998 elif item
in ['to_be_deleted']:
1999 return self
.__dict
__['to_be_deleted']
2001 elif item
in ['nc_dnstr']:
2002 return self
.__dict
__['nc_dnstr']
2004 elif item
in ['update_flags']:
2005 return self
.__dict
__['update_flags']
2007 raise AttributeError("Unknown attribute %s" % item
)
2009 def is_modified(self
):
2010 return (self
.update_flags
!= 0x0)
2012 def set_unmodified(self
):
2013 self
.__dict
__['update_flags'] = 0x0
2016 class SiteLink(object):
2017 """Class defines a site link found under sites
2020 def __init__(self
, dnstr
):
2023 self
.system_flags
= 0
2025 self
.schedule
= None
2026 self
.interval
= None
2030 '''Debug dump string output of Transport object'''
2032 text
= "%s:\n\tdn=%s" % (self
.__class
__.__name
__, self
.dnstr
)
2033 text
= text
+ "\n\toptions=%d" % self
.options
2034 text
= text
+ "\n\tsystem_flags=%d" % self
.system_flags
2035 text
= text
+ "\n\tcost=%d" % self
.cost
2036 text
= text
+ "\n\tinterval=%s" % self
.interval
2038 if self
.schedule
is not None:
2039 text
+= "\n\tschedule.size=%s" % self
.schedule
.size
2040 text
+= "\n\tschedule.bandwidth=%s" % self
.schedule
.bandwidth
2041 text
+= ("\n\tschedule.numberOfSchedules=%s" %
2042 self
.schedule
.numberOfSchedules
)
2044 for i
, header
in enumerate(self
.schedule
.headerArray
):
2045 text
+= ("\n\tschedule.headerArray[%d].type=%d" %
2047 text
+= ("\n\tschedule.headerArray[%d].offset=%d" %
2049 text
= text
+ "\n\tschedule.dataArray[%d].slots[ " % i
2050 for slot
in self
.schedule
.dataArray
[i
].slots
:
2051 text
= text
+ "0x%X " % slot
2054 for dnstr
in self
.site_list
:
2055 text
= text
+ "\n\tsite_list=%s" % dnstr
2058 def load_sitelink(self
, samdb
):
2059 """Given a siteLink object with an prior initialization
2060 for the object's DN, search for the DN and load attributes
2070 res
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
,
2071 attrs
=attrs
, controls
=['extended_dn:0'])
2073 except ldb
.LdbError
, (enum
, estr
):
2074 raise Exception("Unable to find SiteLink for (%s) - (%s)" %
2079 if "options" in msg
:
2080 self
.options
= int(msg
["options"][0])
2082 if "systemFlags" in msg
:
2083 self
.system_flags
= int(msg
["systemFlags"][0])
2086 self
.cost
= int(msg
["cost"][0])
2088 if "replInterval" in msg
:
2089 self
.interval
= int(msg
["replInterval"][0])
2091 if "siteList" in msg
:
2092 for value
in msg
["siteList"]:
2093 dsdn
= dsdb_Dn(samdb
, value
)
2094 guid
= misc
.GUID(dsdn
.dn
.get_extended_component('GUID'))
2095 if guid
not in self
.site_list
:
2096 self
.site_list
.append(guid
)
2098 if "schedule" in msg
:
2099 self
.schedule
= ndr_unpack(drsblobs
.schedule
, value
)
2101 self
.schedule
= new_connection_schedule()
2104 class KCCFailedObject(object):
2105 def __init__(self
, uuid
, failure_count
, time_first_failure
,
2106 last_result
, dns_name
):
2108 self
.failure_count
= failure_count
2109 self
.time_first_failure
= time_first_failure
2110 self
.last_result
= last_result
2111 self
.dns_name
= dns_name
2114 class ReplInfo(object):
2119 self
.schedule
= None
2122 ##################################################
2123 # Global Functions and Variables
2124 ##################################################
2125 MAX_DWORD
= 2 ** 32 - 1
2128 def get_dsa_config_rep(dsa
):
2129 # Find configuration NC replica for the DSA
2130 for c_rep
in dsa
.current_rep_table
.values():
2131 if c_rep
.is_config():
2134 raise KCCError("Unable to find config NC replica for (%s)" %
2138 def sort_dsa_by_guid(dsa1
, dsa2
):
2139 "use ndr_pack for GUID comparison, as appears correct in some places"""
2140 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
2143 def total_schedule(schedule):
2144 if schedule is None:
2145 return 84 * 8 # 84 bytes = 84 * 8 bits
2148 for byte in schedule:
2155 def new_connection_schedule():
2156 """Create a default schedule for an NTDSConnection or Sitelink. This
2157 is packed differently from the repltimes schedule used elsewhere
2158 in KCC (where the 168 nibbles are packed into 84 bytes).
2160 # 168 byte instances of the 0x01 value. The low order 4 bits
2161 # of the byte equate to 15 minute intervals within a single hour.
2162 # There are 168 bytes because there are 168 hours in a full week
2163 # Effectively we are saying to perform replication at the end of
2164 # each hour of the week
2165 schedule = drsblobs.schedule()
2168 schedule.bandwidth = 0
2169 schedule.numberOfSchedules = 1
2171 header = drsblobs.scheduleHeader()
2175 schedule.headerArray = [header]
2177 data = drsblobs.scheduleSlots()
2178 data.slots = [0x01] * 168
2180 schedule.dataArray = [data]