1 # KCC topology utilities
3 # Copyright (C) Dave Craft 2011
4 # Copyright (C) Jelmer Vernooij 2011
5 # Copyright (C) Andrew Bartlett 2015
7 # Andrew Bartlett's alleged work performed by his underlings Douglas
8 # Bagnall and Garming Sam.
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 from samba
import dsdb
27 from samba
.dcerpc
import (
32 from samba
.common
import dsdb_Dn
33 from samba
.ndr
import ndr_unpack
, ndr_pack
36 class KCCError(Exception):
41 (unknown
, schema
, domain
, config
, application
) = range(0, 5)
43 # map the NCType enum to strings for debugging
44 nctype_lut
= dict((v
, k
) for k
, v
in NCType
.__dict
__.items() if k
[:2] != '__')
47 class NamingContext(object):
48 """Base class for a naming context.
50 Holds the DN, GUID, SID (if available) and type of the DN.
51 Subclasses may inherit from this and specialize
54 def __init__(self
, nc_dnstr
):
55 """Instantiate a NamingContext
57 :param nc_dnstr: NC dn string
59 self
.nc_dnstr
= nc_dnstr
62 self
.nc_type
= NCType
.unknown
65 '''Debug dump string output of class'''
66 text
= "%s:" % (self
.__class
__.__name
__,)
67 text
= text
+ "\n\tnc_dnstr=%s" % self
.nc_dnstr
68 text
= text
+ "\n\tnc_guid=%s" % str(self
.nc_guid
)
70 if self
.nc_sid
is None:
71 text
= text
+ "\n\tnc_sid=<absent>"
73 text
= text
+ "\n\tnc_sid=<present>"
75 text
= text
+ "\n\tnc_type=%s (%s)" % (nctype_lut
[self
.nc_type
],
79 def load_nc(self
, samdb
):
80 attrs
= ["objectGUID",
83 res
= samdb
.search(base
=self
.nc_dnstr
,
84 scope
=ldb
.SCOPE_BASE
, attrs
=attrs
)
86 except ldb
.LdbError
, (enum
, estr
):
87 raise KCCError("Unable to find naming context (%s) - (%s)" %
88 (self
.nc_dnstr
, estr
))
90 if "objectGUID" in msg
:
91 self
.nc_guid
= misc
.GUID(samdb
.schema_format_value("objectGUID",
92 msg
["objectGUID"][0]))
93 if "objectSid" in msg
:
94 self
.nc_sid
= msg
["objectSid"][0]
96 assert self
.nc_guid
is not None
99 '''Return True if NC is 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
= []
202 # The (is present) test is a combination of being
203 # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or
204 # hasPartialReplicaNCs) as well as its replica flags found
205 # thru the msDS-HasInstantiatedNCs. If the NC replica meets
206 # the first enumeration test then this flag is set true
207 self
.rep_present_criteria_one
= False
209 # Call my super class we inherited from
210 NamingContext
.__init
__(self
, nc_dnstr
)
213 '''Debug dump string output of class'''
214 text
= "%s:" % self
.__class
__.__name
__
215 text
= text
+ "\n\tdsa_dnstr=%s" % self
.rep_dsa_dnstr
216 text
= text
+ "\n\tdsa_guid=%s" % self
.rep_dsa_guid
217 text
= text
+ "\n\tdefault=%s" % self
.rep_default
218 text
= text
+ "\n\tro=%s" % self
.rep_ro
219 text
= text
+ "\n\tpartial=%s" % self
.rep_partial
220 text
= text
+ "\n\tpresent=%s" % self
.is_present()
221 text
= text
+ "\n\tfsmo_role_owner=%s" % self
.rep_fsmo_role_owner
223 for rep
in self
.rep_repsFrom
:
224 text
= text
+ "\n%s" % rep
226 for rep
in self
.rep_repsTo
:
227 text
= text
+ "\n%s" % rep
229 return "%s\n%s" % (NamingContext
.__str
__(self
), text
)
231 def set_instantiated_flags(self
, flags
=None):
232 '''Set or clear NC replica instantiated flags'''
234 self
.rep_instantiated_flags
= 0
236 self
.rep_instantiated_flags
= flags
238 def identify_by_dsa_attr(self
, samdb
, attr
):
239 """Given an NC which has been discovered thru the
240 nTDSDSA database object, determine what type of NC
241 replica it is (i.e. partial, read only, default)
243 :param attr: attr of nTDSDSA object where NC DN appears
245 # If the NC was found under hasPartialReplicaNCs
246 # then a partial replica at this dsa
247 if attr
== "hasPartialReplicaNCs":
248 self
.rep_partial
= True
249 self
.rep_present_criteria_one
= True
251 # If the NC is listed under msDS-HasDomainNCs then
252 # this can only be a domain NC and it is the DSA's
254 elif attr
== "msDS-HasDomainNCs":
255 self
.rep_default
= True
257 # NCs listed under hasMasterNCs are either
258 # default domain, schema, or config. We check
259 # against schema and config because they will be
260 # the same for all nTDSDSAs in the forest. That
261 # leaves the default domain NC remaining which
262 # may be different for each nTDSDSAs (and thus
263 # we don't compare agains this samdb's default
265 elif attr
== "hasMasterNCs":
266 self
.rep_present_criteria_one
= True
268 if self
.nc_dnstr
!= str(samdb
.get_schema_basedn()) and \
269 self
.nc_dnstr
!= str(samdb
.get_config_basedn()):
270 self
.rep_default
= True
273 elif attr
== "msDS-hasFullReplicaNCs":
274 self
.rep_present_criteria_one
= True
278 elif attr
== "msDS-hasMasterNCs":
279 self
.rep_present_criteria_one
= True
282 # Now use this DSA attribute to identify the naming
283 # context type by calling the super class method
285 NamingContext
.identify_by_dsa_attr(self
, samdb
, attr
)
287 def is_default(self
):
288 """Whether this is a default domain for the dsa that this NC appears on
290 return self
.rep_default
293 '''Return True if NC replica is read only'''
296 def is_partial(self
):
297 '''Return True if NC replica is partial'''
298 return self
.rep_partial
300 def is_present(self
):
301 """Given an NC replica which has been discovered thru the
302 nTDSDSA database object and populated with replica flags
303 from the msDS-HasInstantiatedNCs; return whether the NC
304 replica is present (true) or if the IT_NC_GOING flag is
305 set then the NC replica is not present (false)
307 if self
.rep_present_criteria_one
and \
308 self
.rep_instantiated_flags
& dsdb
.INSTANCE_TYPE_NC_GOING
== 0:
312 def load_repsFrom(self
, samdb
):
313 """Given an NC replica which has been discovered thru the nTDSDSA
314 database object, load the repsFrom attribute for the local replica.
315 held by my dsa. The repsFrom attribute is not replicated so this
316 attribute is relative only to the local DSA that the samdb exists on
319 res
= samdb
.search(base
=self
.nc_dnstr
, scope
=ldb
.SCOPE_BASE
,
322 except ldb
.LdbError
, (enum
, estr
):
323 raise KCCError("Unable to find NC for (%s) - (%s)" %
324 (self
.nc_dnstr
, estr
))
328 # Possibly no repsFrom if this is a singleton DC
329 if "repsFrom" in msg
:
330 for value
in msg
["repsFrom"]:
331 rep
= RepsFromTo(self
.nc_dnstr
,
332 ndr_unpack(drsblobs
.repsFromToBlob
, value
))
333 self
.rep_repsFrom
.append(rep
)
335 def commit_repsFrom(self
, samdb
, ro
=False):
336 """Commit repsFrom to the database"""
338 # XXX - This is not truly correct according to the MS-TECH
339 # docs. To commit a repsFrom we should be using RPCs
340 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
341 # IDL_DRSReplicaDel to affect a repsFrom change.
343 # Those RPCs are missing in samba, so I'll have to
344 # implement them to get this to more accurately
345 # reflect the reference docs. As of right now this
346 # commit to the database will work as its what the
352 for repsFrom
in self
.rep_repsFrom
:
354 # Leave out any to be deleted from
355 # replacement list. Build a list
356 # of to be deleted reps which we will
357 # remove from rep_repsFrom list below
358 if repsFrom
.to_be_deleted
:
359 delreps
.append(repsFrom
)
363 if repsFrom
.is_modified():
364 repsFrom
.set_unmodified()
367 # current (unmodified) elements also get
368 # appended here but no changes will occur
369 # unless something is "to be modified" or
371 newreps
.append(ndr_pack(repsFrom
.ndr_blob
))
373 # Now delete these from our list of rep_repsFrom
374 for repsFrom
in delreps
:
375 self
.rep_repsFrom
.remove(repsFrom
)
378 # Nothing to do if no reps have been modified or
379 # need to be deleted or input option has informed
380 # us to be "readonly" (ro). Leave database
386 m
.dn
= ldb
.Dn(samdb
, self
.nc_dnstr
)
389 ldb
.MessageElement(newreps
, ldb
.FLAG_MOD_REPLACE
, "repsFrom")
394 except ldb
.LdbError
, estr
:
395 raise KCCError("Could not set repsFrom for (%s) - (%s)" %
396 (self
.nc_dnstr
, estr
))
398 def load_replUpToDateVector(self
, samdb
):
399 """Given an NC replica which has been discovered thru the nTDSDSA
400 database object, load the replUpToDateVector attribute for the
401 local replica. held by my dsa. The replUpToDateVector
402 attribute is not replicated so this attribute is relative only
403 to the local DSA that the samdb exists on
407 res
= samdb
.search(base
=self
.nc_dnstr
, scope
=ldb
.SCOPE_BASE
,
408 attrs
=["replUpToDateVector"])
410 except ldb
.LdbError
, (enum
, estr
):
411 raise KCCError("Unable to find NC for (%s) - (%s)" %
412 (self
.nc_dnstr
, estr
))
416 # Possibly no replUpToDateVector if this is a singleton DC
417 if "replUpToDateVector" in msg
:
418 value
= msg
["replUpToDateVector"][0]
419 blob
= ndr_unpack(drsblobs
.replUpToDateVectorBlob
,
421 if blob
.version
!= 2:
422 # Samba only generates version 2, and this runs locally
423 raise AttributeError("Unexpected replUpToDateVector version %d"
426 self
.rep_replUpToDateVector_cursors
= blob
.ctr
.cursors
428 self
.rep_replUpToDateVector_cursors
= []
430 def dumpstr_to_be_deleted(self
):
431 return '\n'.join(str(x
) for x
in self
.rep_repsFrom
if x
.to_be_deleted
)
433 def dumpstr_to_be_modified(self
):
434 return '\n'.join(str(x
) for x
in self
.rep_repsFrom
if x
.is_modified())
436 def dumpstr_reps_to(self
):
437 return '\n'.join(str(x
) for x
in self
.rep_repsTo
if x
.to_be_deleted
)
439 def load_fsmo_roles(self
, samdb
):
440 """Given an NC replica which has been discovered thru the nTDSDSA
441 database object, load the fSMORoleOwner attribute.
444 res
= samdb
.search(base
=self
.nc_dnstr
, scope
=ldb
.SCOPE_BASE
,
445 attrs
=["fSMORoleOwner"])
447 except ldb
.LdbError
, (enum
, estr
):
448 raise KCCError("Unable to find NC for (%s) - (%s)" %
449 (self
.nc_dnstr
, estr
))
453 # Possibly no fSMORoleOwner
454 if "fSMORoleOwner" in msg
:
455 self
.rep_fsmo_role_owner
= msg
["fSMORoleOwner"]
457 def is_fsmo_role_owner(self
, dsa_dnstr
):
458 if self
.rep_fsmo_role_owner
is not None and \
459 self
.rep_fsmo_role_owner
== dsa_dnstr
:
463 def load_repsTo(self
, samdb
):
464 """Given an NC replica which has been discovered thru the nTDSDSA
465 database object, load the repsTo attribute for the local replica.
466 held by my dsa. The repsTo attribute is not replicated so this
467 attribute is relative only to the local DSA that the samdb exists on
469 This is responsible for push replication, not scheduled pull
470 replication. Not to be confused for repsFrom.
473 res
= samdb
.search(base
=self
.nc_dnstr
, scope
=ldb
.SCOPE_BASE
,
476 except ldb
.LdbError
, (enum
, estr
):
477 raise KCCError("Unable to find NC for (%s) - (%s)" %
478 (self
.nc_dnstr
, estr
))
482 # Possibly no repsTo if this is a singleton DC
484 for value
in msg
["repsTo"]:
485 rep
= RepsFromTo(self
.nc_dnstr
,
486 ndr_unpack(drsblobs
.repsFromToBlob
, value
))
487 self
.rep_repsTo
.append(rep
)
489 def commit_repsTo(self
, samdb
, ro
=False):
490 """Commit repsTo to the database"""
492 # XXX - This is not truly correct according to the MS-TECH
493 # docs. To commit a repsTo we should be using RPCs
494 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
495 # IDL_DRSReplicaDel to affect a repsTo change.
497 # Those RPCs are missing in samba, so I'll have to
498 # implement them to get this to more accurately
499 # reflect the reference docs. As of right now this
500 # commit to the database will work as its what the
506 for repsTo
in self
.rep_repsTo
:
508 # Leave out any to be deleted from
509 # replacement list. Build a list
510 # of to be deleted reps which we will
511 # remove from rep_repsTo list below
512 if repsTo
.to_be_deleted
:
513 delreps
.append(repsTo
)
517 if repsTo
.is_modified():
518 repsTo
.set_unmodified()
521 # current (unmodified) elements also get
522 # appended here but no changes will occur
523 # unless something is "to be modified" or
525 newreps
.append(ndr_pack(repsTo
.ndr_blob
))
527 # Now delete these from our list of rep_repsTo
528 for repsTo
in delreps
:
529 self
.rep_repsTo
.remove(repsTo
)
532 # Nothing to do if no reps have been modified or
533 # need to be deleted or input option has informed
534 # us to be "readonly" (ro). Leave database
540 m
.dn
= ldb
.Dn(samdb
, self
.nc_dnstr
)
543 ldb
.MessageElement(newreps
, ldb
.FLAG_MOD_REPLACE
, "repsTo")
548 except ldb
.LdbError
, estr
:
549 raise KCCError("Could not set repsTo for (%s) - (%s)" %
550 (self
.nc_dnstr
, estr
))
553 class DirectoryServiceAgent(object):
555 def __init__(self
, dsa_dnstr
):
556 """Initialize DSA class.
558 Class is subsequently fully populated by calling the load_dsa() method
560 :param dsa_dnstr: DN of the nTDSDSA
562 self
.dsa_dnstr
= dsa_dnstr
565 self
.dsa_is_ro
= False
566 self
.dsa_is_istg
= False
568 self
.dsa_behavior
= 0
569 self
.default_dnstr
= None # default domain dn string for dsa
571 # NCReplicas for this dsa that are "present"
572 # Indexed by DN string of naming context
573 self
.current_rep_table
= {}
575 # NCReplicas for this dsa that "should be present"
576 # Indexed by DN string of naming context
577 self
.needed_rep_table
= {}
579 # NTDSConnections for this dsa. These are current
580 # valid connections that are committed or pending a commit
581 # in the database. Indexed by DN string of connection
582 self
.connect_table
= {}
585 '''Debug dump string output of class'''
587 text
= "%s:" % self
.__class
__.__name
__
588 if self
.dsa_dnstr
is not None:
589 text
= text
+ "\n\tdsa_dnstr=%s" % self
.dsa_dnstr
590 if self
.dsa_guid
is not None:
591 text
= text
+ "\n\tdsa_guid=%s" % str(self
.dsa_guid
)
592 if self
.dsa_ivid
is not None:
593 text
= text
+ "\n\tdsa_ivid=%s" % str(self
.dsa_ivid
)
595 text
= text
+ "\n\tro=%s" % self
.is_ro()
596 text
= text
+ "\n\tgc=%s" % self
.is_gc()
597 text
= text
+ "\n\tistg=%s" % self
.is_istg()
599 text
= text
+ "\ncurrent_replica_table:"
600 text
= text
+ "\n%s" % self
.dumpstr_current_replica_table()
601 text
= text
+ "\nneeded_replica_table:"
602 text
= text
+ "\n%s" % self
.dumpstr_needed_replica_table()
603 text
= text
+ "\nconnect_table:"
604 text
= text
+ "\n%s" % self
.dumpstr_connect_table()
608 def get_current_replica(self
, nc_dnstr
):
609 return self
.current_rep_table
.get(nc_dnstr
)
612 '''Returns True if dsa is intersite topology generator for it's site'''
613 # The KCC on an RODC always acts as an ISTG for itself
614 return self
.dsa_is_istg
or self
.dsa_is_ro
617 '''Returns True if dsa a read only domain controller'''
618 return self
.dsa_is_ro
621 '''Returns True if dsa hosts a global catalog'''
622 if (self
.options
& dsdb
.DS_NTDSDSA_OPT_IS_GC
) != 0:
626 def is_minimum_behavior(self
, version
):
627 """Is dsa at minimum windows level greater than or equal to (version)
629 :param version: Windows version to test against
630 (e.g. DS_DOMAIN_FUNCTION_2008)
632 if self
.dsa_behavior
>= version
:
636 def is_translate_ntdsconn_disabled(self
):
637 """Whether this allows NTDSConnection translation in its options."""
638 if (self
.options
& dsdb
.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE
) != 0:
642 def get_rep_tables(self
):
643 """Return DSA current and needed replica tables
645 return self
.current_rep_table
, self
.needed_rep_table
647 def get_parent_dnstr(self
):
648 """Get the parent DN string of this object."""
649 head
, sep
, tail
= self
.dsa_dnstr
.partition(',')
652 def load_dsa(self
, samdb
):
653 """Load a DSA from the samdb.
655 Prior initialization has given us the DN of the DSA that we are to
656 load. This method initializes all other attributes, including loading
657 the NC replica table for this DSA.
659 attrs
= ["objectGUID",
663 "msDS-Behavior-Version"]
665 res
= samdb
.search(base
=self
.dsa_dnstr
, scope
=ldb
.SCOPE_BASE
,
668 except ldb
.LdbError
, (enum
, estr
):
669 raise KCCError("Unable to find nTDSDSA for (%s) - (%s)" %
670 (self
.dsa_dnstr
, estr
))
673 self
.dsa_guid
= misc
.GUID(samdb
.schema_format_value("objectGUID",
674 msg
["objectGUID"][0]))
676 # RODCs don't originate changes and thus have no invocationId,
677 # therefore we must check for existence first
678 if "invocationId" in msg
:
679 self
.dsa_ivid
= misc
.GUID(samdb
.schema_format_value("objectGUID",
680 msg
["invocationId"][0]))
683 self
.options
= int(msg
["options"][0])
685 if "msDS-isRODC" in msg
and msg
["msDS-isRODC"][0] == "TRUE":
686 self
.dsa_is_ro
= True
688 self
.dsa_is_ro
= False
690 if "msDS-Behavior-Version" in msg
:
691 self
.dsa_behavior
= int(msg
['msDS-Behavior-Version'][0])
693 # Load the NC replicas that are enumerated on this dsa
694 self
.load_current_replica_table(samdb
)
696 # Load the nTDSConnection that are enumerated on this dsa
697 self
.load_connection_table(samdb
)
699 def load_current_replica_table(self
, samdb
):
700 """Method to load the NC replica's listed for DSA object.
702 This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs,
703 hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, and
704 msDS-HasInstantiatedNCs) to determine complete list of NC replicas that
705 are enumerated for the DSA. Once a NC replica is loaded it is
706 identified (schema, config, etc) and the other replica attributes
707 (partial, ro, etc) are determined.
709 :param samdb: database to query for DSA replica list
712 # not RODC - default, config, schema (old style)
714 # not RODC - default, config, schema, app NCs
716 # domain NC partial replicas
717 "hasPartialReplicaNCs",
720 # RODC only - default, config, schema, app NCs
721 "msDS-hasFullReplicaNCs",
722 # Identifies if replica is coming, going, or stable
723 "msDS-HasInstantiatedNCs"
726 res
= samdb
.search(base
=self
.dsa_dnstr
, scope
=ldb
.SCOPE_BASE
,
729 except ldb
.LdbError
, (enum
, estr
):
730 raise KCCError("Unable to find nTDSDSA NCs for (%s) - (%s)" %
731 (self
.dsa_dnstr
, estr
))
733 # The table of NCs for the dsa we are searching
736 # We should get one response to our query here for
737 # the ntds that we requested
740 # Our response will contain a number of elements including
741 # the dn of the dsa as well as elements for each
742 # attribute (e.g. hasMasterNCs). Each of these elements
743 # is a dictonary list which we retrieve the keys for and
744 # then iterate over them
745 for k
in res
[0].keys():
749 # For each attribute type there will be one or more DNs
750 # listed. For instance DCs normally have 3 hasMasterNCs
752 for value
in res
[0][k
]:
753 # Turn dn into a dsdb_Dn so we can use
754 # its methods to parse a binary DN
755 dsdn
= dsdb_Dn(samdb
, value
)
756 flags
= dsdn
.get_binary_integer()
759 if not dnstr
in tmp_table
:
760 rep
= NCReplica(self
.dsa_dnstr
, self
.dsa_guid
, dnstr
)
761 tmp_table
[dnstr
] = rep
763 rep
= tmp_table
[dnstr
]
765 if k
== "msDS-HasInstantiatedNCs":
766 rep
.set_instantiated_flags(flags
)
769 rep
.identify_by_dsa_attr(samdb
, k
)
771 # if we've identified the default domain NC
772 # then save its DN string
774 self
.default_dnstr
= dnstr
776 raise KCCError("No nTDSDSA NCs for (%s)" % self
.dsa_dnstr
)
778 # Assign our newly built NC replica table to this dsa
779 self
.current_rep_table
= tmp_table
781 def add_needed_replica(self
, rep
):
782 """Method to add a NC replica that "should be present" to the
785 self
.needed_rep_table
[rep
.nc_dnstr
] = rep
787 def load_connection_table(self
, samdb
):
788 """Method to load the nTDSConnections listed for DSA object.
790 :param samdb: database to query for DSA connection list
793 res
= samdb
.search(base
=self
.dsa_dnstr
,
794 scope
=ldb
.SCOPE_SUBTREE
,
795 expression
="(objectClass=nTDSConnection)")
797 except ldb
.LdbError
, (enum
, estr
):
798 raise KCCError("Unable to find nTDSConnection for (%s) - (%s)" %
799 (self
.dsa_dnstr
, estr
))
805 if dnstr
in self
.connect_table
:
808 connect
= NTDSConnection(dnstr
)
810 connect
.load_connection(samdb
)
811 self
.connect_table
[dnstr
] = connect
813 def commit_connections(self
, samdb
, ro
=False):
814 """Method to commit any uncommitted nTDSConnections
815 modifications that are in our table. These would be
816 identified connections that are marked to be added or
819 :param samdb: database to commit DSA connection list to
820 :param ro: if (true) then peform internal operations but
821 do not write to the database (readonly)
825 for dnstr
, connect
in self
.connect_table
.items():
826 if connect
.to_be_added
:
827 connect
.commit_added(samdb
, ro
)
829 if connect
.to_be_modified
:
830 connect
.commit_modified(samdb
, ro
)
832 if connect
.to_be_deleted
:
833 connect
.commit_deleted(samdb
, ro
)
834 delconn
.append(dnstr
)
836 # Now delete the connection from the table
837 for dnstr
in delconn
:
838 del self
.connect_table
[dnstr
]
840 def add_connection(self
, dnstr
, connect
):
841 assert dnstr
not in self
.connect_table
842 self
.connect_table
[dnstr
] = connect
844 def get_connection_by_from_dnstr(self
, from_dnstr
):
845 """Scan DSA nTDSConnection table and return connection
846 with a "fromServer" dn string equivalent to method
849 :param from_dnstr: search for this from server entry
852 for connect
in self
.connect_table
.values():
853 if connect
.get_from_dnstr() == from_dnstr
:
854 answer
.append(connect
)
858 def dumpstr_current_replica_table(self
):
859 '''Debug dump string output of current replica table'''
860 return '\n'.join(str(x
) for x
in self
.current_rep_table
)
862 def dumpstr_needed_replica_table(self
):
863 '''Debug dump string output of needed replica table'''
864 return '\n'.join(str(x
) for x
in self
.needed_rep_table
)
866 def dumpstr_connect_table(self
):
867 '''Debug dump string output of connect table'''
868 return '\n'.join(str(x
) for x
in self
.connect_table
)
870 def new_connection(self
, options
, system_flags
, transport
, from_dnstr
,
872 """Set up a new connection for the DSA based on input
873 parameters. Connection will be added to the DSA
874 connect_table and will be marked as "to be added" pending
875 a call to commit_connections()
877 dnstr
= "CN=%s," % str(uuid
.uuid4()) + self
.dsa_dnstr
879 connect
= NTDSConnection(dnstr
)
880 connect
.to_be_added
= True
881 connect
.enabled
= True
882 connect
.from_dnstr
= from_dnstr
883 connect
.options
= options
884 connect
.system_flags
= system_flags
886 if transport
is not None:
887 connect
.transport_dnstr
= transport
.dnstr
888 connect
.transport_guid
= transport
.guid
890 if sched
is not None:
891 connect
.schedule
= sched
893 # Create schedule. Attribute valuse set according to MS-TECH
894 # intrasite connection creation document
895 connect
.schedule
= new_connection_schedule()
897 self
.add_connection(dnstr
, connect
)
901 class NTDSConnection(object):
902 """Class defines a nTDSConnection found under a DSA
904 def __init__(self
, dnstr
):
909 self
.to_be_added
= False # new connection needs to be added
910 self
.to_be_deleted
= False # old connection needs to be deleted
911 self
.to_be_modified
= False
913 self
.system_flags
= 0
914 self
.transport_dnstr
= None
915 self
.transport_guid
= None
916 self
.from_dnstr
= None
920 '''Debug dump string output of NTDSConnection object'''
922 text
= "%s:\n\tdn=%s" % (self
.__class
__.__name
__, self
.dnstr
)
923 text
= text
+ "\n\tenabled=%s" % self
.enabled
924 text
= text
+ "\n\tto_be_added=%s" % self
.to_be_added
925 text
= text
+ "\n\tto_be_deleted=%s" % self
.to_be_deleted
926 text
= text
+ "\n\tto_be_modified=%s" % self
.to_be_modified
927 text
= text
+ "\n\toptions=0x%08X" % self
.options
928 text
= text
+ "\n\tsystem_flags=0x%08X" % self
.system_flags
929 text
= text
+ "\n\twhenCreated=%d" % self
.whenCreated
930 text
= text
+ "\n\ttransport_dn=%s" % self
.transport_dnstr
932 if self
.guid
is not None:
933 text
= text
+ "\n\tguid=%s" % str(self
.guid
)
935 if self
.transport_guid
is not None:
936 text
= text
+ "\n\ttransport_guid=%s" % str(self
.transport_guid
)
938 text
= text
+ "\n\tfrom_dn=%s" % self
.from_dnstr
940 if self
.schedule
is not None:
941 text
+= "\n\tschedule.size=%s" % self
.schedule
.size
942 text
+= "\n\tschedule.bandwidth=%s" % self
.schedule
.bandwidth
943 text
+= ("\n\tschedule.numberOfSchedules=%s" %
944 self
.schedule
.numberOfSchedules
)
946 for i
, header
in enumerate(self
.schedule
.headerArray
):
947 text
+= ("\n\tschedule.headerArray[%d].type=%d" %
949 text
+= ("\n\tschedule.headerArray[%d].offset=%d" %
951 text
+= "\n\tschedule.dataArray[%d].slots[ " % i
952 for slot
in self
.schedule
.dataArray
[i
].slots
:
953 text
= text
+ "0x%X " % slot
958 def load_connection(self
, samdb
):
959 """Given a NTDSConnection object with an prior initialization
960 for the object's DN, search for the DN and load attributes
972 res
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
,
975 except ldb
.LdbError
, (enum
, estr
):
976 raise KCCError("Unable to find nTDSConnection for (%s) - (%s)" %
982 self
.options
= int(msg
["options"][0])
984 if "enabledConnection" in msg
:
985 if msg
["enabledConnection"][0].upper().lstrip().rstrip() == "TRUE":
988 if "systemFlags" in msg
:
989 self
.system_flags
= int(msg
["systemFlags"][0])
993 misc
.GUID(samdb
.schema_format_value("objectGUID",
994 msg
["objectGUID"][0]))
996 raise KCCError("Unable to find objectGUID in nTDSConnection "
997 "for (%s)" % (self
.dnstr
))
999 if "transportType" in msg
:
1000 dsdn
= dsdb_Dn(samdb
, msg
["transportType"][0])
1001 self
.load_connection_transport(samdb
, str(dsdn
.dn
))
1003 if "schedule" in msg
:
1004 self
.schedule
= ndr_unpack(drsblobs
.schedule
, msg
["schedule"][0])
1006 if "whenCreated" in msg
:
1007 self
.whenCreated
= ldb
.string_to_time(msg
["whenCreated"][0])
1009 if "fromServer" in msg
:
1010 dsdn
= dsdb_Dn(samdb
, msg
["fromServer"][0])
1011 self
.from_dnstr
= str(dsdn
.dn
)
1012 assert self
.from_dnstr
is not None
1014 def load_connection_transport(self
, samdb
, tdnstr
):
1015 """Given a NTDSConnection object which enumerates a transport
1016 DN, load the transport information for the connection object
1018 :param tdnstr: transport DN to load
1020 attrs
= ["objectGUID"]
1022 res
= samdb
.search(base
=tdnstr
,
1023 scope
=ldb
.SCOPE_BASE
, attrs
=attrs
)
1025 except ldb
.LdbError
, (enum
, estr
):
1026 raise KCCError("Unable to find transport (%s) - (%s)" %
1029 if "objectGUID" in res
[0]:
1031 self
.transport_dnstr
= tdnstr
1032 self
.transport_guid
= \
1033 misc
.GUID(samdb
.schema_format_value("objectGUID",
1034 msg
["objectGUID"][0]))
1035 assert self
.transport_dnstr
is not None
1036 assert self
.transport_guid
is not None
1038 def commit_deleted(self
, samdb
, ro
=False):
1039 """Local helper routine for commit_connections() which
1040 handles committed connections that are to be deleted from
1041 the database database
1043 assert self
.to_be_deleted
1044 self
.to_be_deleted
= False
1046 # No database modification requested
1051 samdb
.delete(self
.dnstr
)
1052 except ldb
.LdbError
, (enum
, estr
):
1053 raise KCCError("Could not delete nTDSConnection for (%s) - (%s)" %
1056 def commit_added(self
, samdb
, ro
=False):
1057 """Local helper routine for commit_connections() which
1058 handles committed connections that are to be added to the
1061 assert self
.to_be_added
1062 self
.to_be_added
= False
1064 # No database modification requested
1068 # First verify we don't have this entry to ensure nothing
1069 # is programatically amiss
1072 msg
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
)
1076 except ldb
.LdbError
, (enum
, estr
):
1077 if enum
!= ldb
.ERR_NO_SUCH_OBJECT
:
1078 raise KCCError("Unable to search for (%s) - (%s)" %
1081 raise KCCError("nTDSConnection for (%s) already exists!" %
1089 # Prepare a message for adding to the samdb
1091 m
.dn
= ldb
.Dn(samdb
, self
.dnstr
)
1093 m
["objectClass"] = \
1094 ldb
.MessageElement("nTDSConnection", ldb
.FLAG_MOD_ADD
,
1096 m
["showInAdvancedViewOnly"] = \
1097 ldb
.MessageElement("TRUE", ldb
.FLAG_MOD_ADD
,
1098 "showInAdvancedViewOnly")
1099 m
["enabledConnection"] = \
1100 ldb
.MessageElement(enablestr
, ldb
.FLAG_MOD_ADD
,
1101 "enabledConnection")
1103 ldb
.MessageElement(self
.from_dnstr
, ldb
.FLAG_MOD_ADD
, "fromServer")
1105 ldb
.MessageElement(str(self
.options
), ldb
.FLAG_MOD_ADD
, "options")
1106 m
["systemFlags"] = \
1107 ldb
.MessageElement(str(self
.system_flags
), ldb
.FLAG_MOD_ADD
,
1110 if self
.transport_dnstr
is not None:
1111 m
["transportType"] = \
1112 ldb
.MessageElement(str(self
.transport_dnstr
), ldb
.FLAG_MOD_ADD
,
1115 if self
.schedule
is not None:
1117 ldb
.MessageElement(ndr_pack(self
.schedule
),
1118 ldb
.FLAG_MOD_ADD
, "schedule")
1121 except ldb
.LdbError
, (enum
, estr
):
1122 raise KCCError("Could not add nTDSConnection for (%s) - (%s)" %
1125 def commit_modified(self
, samdb
, ro
=False):
1126 """Local helper routine for commit_connections() which
1127 handles committed connections that are to be modified to the
1130 assert self
.to_be_modified
1131 self
.to_be_modified
= False
1133 # No database modification requested
1137 # First verify we have this entry to ensure nothing
1138 # is programatically amiss
1140 # we don't use the search result, but it tests the status
1141 # of self.dnstr in the database.
1142 samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
)
1144 except ldb
.LdbError
, (enum
, estr
):
1145 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
1146 raise KCCError("nTDSConnection for (%s) doesn't exist!" %
1148 raise KCCError("Unable to search for (%s) - (%s)" %
1156 # Prepare a message for modifying the samdb
1158 m
.dn
= ldb
.Dn(samdb
, self
.dnstr
)
1160 m
["enabledConnection"] = \
1161 ldb
.MessageElement(enablestr
, ldb
.FLAG_MOD_REPLACE
,
1162 "enabledConnection")
1164 ldb
.MessageElement(self
.from_dnstr
, ldb
.FLAG_MOD_REPLACE
,
1167 ldb
.MessageElement(str(self
.options
), ldb
.FLAG_MOD_REPLACE
,
1169 m
["systemFlags"] = \
1170 ldb
.MessageElement(str(self
.system_flags
), ldb
.FLAG_MOD_REPLACE
,
1173 if self
.transport_dnstr
is not None:
1174 m
["transportType"] = \
1175 ldb
.MessageElement(str(self
.transport_dnstr
),
1176 ldb
.FLAG_MOD_REPLACE
, "transportType")
1178 m
["transportType"] = \
1179 ldb
.MessageElement([], ldb
.FLAG_MOD_DELETE
, "transportType")
1181 if self
.schedule
is not None:
1183 ldb
.MessageElement(ndr_pack(self
.schedule
),
1184 ldb
.FLAG_MOD_REPLACE
, "schedule")
1187 ldb
.MessageElement([], ldb
.FLAG_MOD_DELETE
, "schedule")
1190 except ldb
.LdbError
, (enum
, estr
):
1191 raise KCCError("Could not modify nTDSConnection for (%s) - (%s)" %
1194 def set_modified(self
, truefalse
):
1195 self
.to_be_modified
= truefalse
1197 def is_schedule_minimum_once_per_week(self
):
1198 """Returns True if our schedule includes at least one
1199 replication interval within the week. False otherwise
1201 # replinfo schedule is None means "always", while
1202 # NTDSConnection schedule is None means "never".
1203 if self
.schedule
is None or self
.schedule
.dataArray
[0] is None:
1206 for slot
in self
.schedule
.dataArray
[0].slots
:
1207 if (slot
& 0x0F) != 0x0:
1211 def is_equivalent_schedule(self
, sched
):
1212 """Returns True if our schedule is equivalent to the input
1213 comparison schedule.
1215 :param shed: schedule to compare to
1217 # There are 4 cases, where either self.schedule or sched can be None
1219 # | self. is None | self. is not None
1220 # --------------+-----------------+--------------------
1221 # sched is None | True | False
1222 # --------------+-----------------+--------------------
1223 # sched is not None | False | do calculations
1225 if self
.schedule
is None:
1226 return sched
is None
1231 if ((self
.schedule
.size
!= sched
.size
or
1232 self
.schedule
.bandwidth
!= sched
.bandwidth
or
1233 self
.schedule
.numberOfSchedules
!= sched
.numberOfSchedules
)):
1236 for i
, header
in enumerate(self
.schedule
.headerArray
):
1238 if self
.schedule
.headerArray
[i
].type != sched
.headerArray
[i
].type:
1241 if self
.schedule
.headerArray
[i
].offset
!= \
1242 sched
.headerArray
[i
].offset
:
1245 for a
, b
in zip(self
.schedule
.dataArray
[i
].slots
,
1246 sched
.dataArray
[i
].slots
):
1251 def is_rodc_topology(self
):
1252 """Returns True if NTDS Connection specifies RODC
1255 if self
.options
& dsdb
.NTDSCONN_OPT_RODC_TOPOLOGY
== 0:
1259 def is_generated(self
):
1260 """Returns True if NTDS Connection was generated by the
1261 KCC topology algorithm as opposed to set by the administrator
1263 if self
.options
& dsdb
.NTDSCONN_OPT_IS_GENERATED
== 0:
1267 def is_override_notify_default(self
):
1268 """Returns True if NTDS Connection should override notify default
1270 if self
.options
& dsdb
.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT
== 0:
1274 def is_use_notify(self
):
1275 """Returns True if NTDS Connection should use notify
1277 if self
.options
& dsdb
.NTDSCONN_OPT_USE_NOTIFY
== 0:
1281 def is_twoway_sync(self
):
1282 """Returns True if NTDS Connection should use twoway sync
1284 if self
.options
& dsdb
.NTDSCONN_OPT_TWOWAY_SYNC
== 0:
1288 def is_intersite_compression_disabled(self
):
1289 """Returns True if NTDS Connection intersite compression
1292 if self
.options
& dsdb
.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
== 0:
1296 def is_user_owned_schedule(self
):
1297 """Returns True if NTDS Connection has a user owned schedule
1299 if self
.options
& dsdb
.NTDSCONN_OPT_USER_OWNED_SCHEDULE
== 0:
1303 def is_enabled(self
):
1304 """Returns True if NTDS Connection is enabled
1308 def get_from_dnstr(self
):
1309 '''Return fromServer dn string attribute'''
1310 return self
.from_dnstr
1313 class Partition(NamingContext
):
1314 """A naming context discovered thru Partitions DN of the config schema.
1316 This is a more specific form of NamingContext class (inheriting from that
1317 class) and it identifies unique attributes enumerated in the Partitions
1318 such as which nTDSDSAs are cross referenced for replicas
1320 def __init__(self
, partstr
):
1321 self
.partstr
= partstr
1323 self
.system_flags
= 0
1324 self
.rw_location_list
= []
1325 self
.ro_location_list
= []
1327 # We don't have enough info to properly
1328 # fill in the naming context yet. We'll get that
1329 # fully set up with load_partition().
1330 NamingContext
.__init
__(self
, None)
1332 def load_partition(self
, samdb
):
1333 """Given a Partition class object that has been initialized with its
1334 partition dn string, load the partition from the sam database, identify
1335 the type of the partition (schema, domain, etc) and record the list of
1336 nTDSDSAs that appear in the cross reference attributes
1337 msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations.
1339 :param samdb: sam database to load partition from
1344 "msDS-NC-Replica-Locations",
1345 "msDS-NC-RO-Replica-Locations"]
1347 res
= samdb
.search(base
=self
.partstr
, scope
=ldb
.SCOPE_BASE
,
1350 except ldb
.LdbError
, (enum
, estr
):
1351 raise KCCError("Unable to find partition for (%s) - (%s)" %
1352 (self
.partstr
, estr
))
1354 for k
in msg
.keys():
1359 if msg
[k
][0].upper().lstrip().rstrip() == "TRUE":
1362 self
.enabled
= False
1365 if k
== "systemFlags":
1366 self
.system_flags
= int(msg
[k
][0])
1369 for value
in msg
[k
]:
1370 dsdn
= dsdb_Dn(samdb
, value
)
1371 dnstr
= str(dsdn
.dn
)
1374 self
.nc_dnstr
= dnstr
1377 if k
== "msDS-NC-Replica-Locations":
1378 self
.rw_location_list
.append(dnstr
)
1381 if k
== "msDS-NC-RO-Replica-Locations":
1382 self
.ro_location_list
.append(dnstr
)
1385 # Now identify what type of NC this partition
1387 self
.identify_by_basedn(samdb
)
1389 def is_enabled(self
):
1390 """Returns True if partition is enabled
1392 return self
.is_enabled
1394 def is_foreign(self
):
1395 """Returns True if this is not an Active Directory NC in our
1396 forest but is instead something else (e.g. a foreign NC)
1398 if (self
.system_flags
& dsdb
.SYSTEM_FLAG_CR_NTDS_NC
) == 0:
1403 def should_be_present(self
, target_dsa
):
1404 """Tests whether this partition should have an NC replica
1405 on the target dsa. This method returns a tuple of
1406 needed=True/False, ro=True/False, partial=True/False
1408 :param target_dsa: should NC be present on target dsa
1413 # If this is the config, schema, or default
1414 # domain NC for the target dsa then it should
1416 needed
= (self
.nc_type
== NCType
.config
or
1417 self
.nc_type
== NCType
.schema
or
1418 (self
.nc_type
== NCType
.domain
and
1419 self
.nc_dnstr
== target_dsa
.default_dnstr
))
1421 # A writable replica of an application NC should be present
1422 # if there a cross reference to the target DSA exists. Depending
1423 # on whether the DSA is ro we examine which type of cross reference
1424 # to look for (msDS-NC-Replica-Locations or
1425 # msDS-NC-RO-Replica-Locations
1426 if self
.nc_type
== NCType
.application
:
1427 if target_dsa
.is_ro():
1428 if target_dsa
.dsa_dnstr
in self
.ro_location_list
:
1431 if target_dsa
.dsa_dnstr
in self
.rw_location_list
:
1434 # If the target dsa is a gc then a partial replica of a
1435 # domain NC (other than the DSAs default domain) should exist
1436 # if there is also a cross reference for the DSA
1437 if (target_dsa
.is_gc() and
1438 self
.nc_type
== NCType
.domain
and
1439 self
.nc_dnstr
!= target_dsa
.default_dnstr
and
1440 (target_dsa
.dsa_dnstr
in self
.ro_location_list
or
1441 target_dsa
.dsa_dnstr
in self
.rw_location_list
)):
1445 # partial NCs are always readonly
1446 if needed
and (target_dsa
.is_ro() or partial
):
1449 return needed
, ro
, partial
1452 '''Debug dump string output of class'''
1453 text
= "%s" % NamingContext
.__str
__(self
)
1454 text
= text
+ "\n\tpartdn=%s" % self
.partstr
1455 for k
in self
.rw_location_list
:
1456 text
= text
+ "\n\tmsDS-NC-Replica-Locations=%s" % k
1457 for k
in self
.ro_location_list
:
1458 text
= text
+ "\n\tmsDS-NC-RO-Replica-Locations=%s" % k
1463 """An individual site object discovered thru the configuration
1464 naming context. Contains all DSAs that exist within the site
1466 def __init__(self
, site_dnstr
, nt_now
):
1467 self
.site_dnstr
= site_dnstr
1468 self
.site_guid
= None
1469 self
.site_options
= 0
1470 self
.site_topo_generator
= None
1471 self
.site_topo_failover
= 0 # appears to be in minutes
1473 self
.rw_dsa_table
= {}
1474 self
.nt_now
= nt_now
1476 def load_site(self
, samdb
):
1477 """Loads the NTDS Site Settings options attribute for the site
1478 as well as querying and loading all DSAs that appear within
1481 ssdn
= "CN=NTDS Site Settings,%s" % self
.site_dnstr
1483 "interSiteTopologyFailover",
1484 "interSiteTopologyGenerator"]
1486 res
= samdb
.search(base
=ssdn
, scope
=ldb
.SCOPE_BASE
,
1488 self_res
= samdb
.search(base
=self
.site_dnstr
, scope
=ldb
.SCOPE_BASE
,
1489 attrs
=['objectGUID'])
1490 except ldb
.LdbError
, (enum
, estr
):
1491 raise KCCError("Unable to find site settings for (%s) - (%s)" %
1495 if "options" in msg
:
1496 self
.site_options
= int(msg
["options"][0])
1498 if "interSiteTopologyGenerator" in msg
:
1499 self
.site_topo_generator
= \
1500 str(msg
["interSiteTopologyGenerator"][0])
1502 if "interSiteTopologyFailover" in msg
:
1503 self
.site_topo_failover
= int(msg
["interSiteTopologyFailover"][0])
1506 if "objectGUID" in msg
:
1507 self
.site_guid
= misc
.GUID(samdb
.schema_format_value("objectGUID",
1508 msg
["objectGUID"][0]))
1510 self
.load_all_dsa(samdb
)
1512 def load_all_dsa(self
, samdb
):
1513 """Discover all nTDSDSA thru the sites entry and
1514 instantiate and load the DSAs. Each dsa is inserted
1515 into the dsa_table by dn string.
1518 res
= samdb
.search(self
.site_dnstr
,
1519 scope
=ldb
.SCOPE_SUBTREE
,
1520 expression
="(objectClass=nTDSDSA)")
1521 except ldb
.LdbError
, (enum
, estr
):
1522 raise KCCError("Unable to find nTDSDSAs - (%s)" % estr
)
1528 if dnstr
in self
.dsa_table
:
1531 dsa
= DirectoryServiceAgent(dnstr
)
1535 # Assign this dsa to my dsa table
1536 # and index by dsa dn
1537 self
.dsa_table
[dnstr
] = dsa
1539 self
.rw_dsa_table
[dnstr
] = dsa
1541 def get_dsa_by_guidstr(self
, guidstr
): # XXX unused
1542 for dsa
in self
.dsa_table
.values():
1543 if str(dsa
.dsa_guid
) == guidstr
:
1547 def get_dsa(self
, dnstr
):
1548 """Return a previously loaded DSA object by consulting
1549 the sites dsa_table for the provided DSA dn string
1551 :return: None if DSA doesn't exist
1553 return self
.dsa_table
.get(dnstr
)
1555 def select_istg(self
, samdb
, mydsa
, ro
):
1556 """Determine if my DC should be an intersite topology
1557 generator. If my DC is the istg and is both a writeable
1558 DC and the database is opened in write mode then we perform
1559 an originating update to set the interSiteTopologyGenerator
1560 attribute in the NTDS Site Settings object. An RODC always
1561 acts as an ISTG for itself.
1563 # The KCC on an RODC always acts as an ISTG for itself
1565 mydsa
.dsa_is_istg
= True
1566 self
.site_topo_generator
= mydsa
.dsa_dnstr
1569 c_rep
= get_dsa_config_rep(mydsa
)
1571 # Load repsFrom and replUpToDateVector if not already loaded
1572 # so we can get the current state of the config replica and
1573 # whether we are getting updates from the istg
1574 c_rep
.load_repsFrom(samdb
)
1576 c_rep
.load_replUpToDateVector(samdb
)
1578 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1579 # First, the KCC on a writable DC determines whether it acts
1580 # as an ISTG for its site
1582 # Let s be the object such that s!lDAPDisplayName = nTDSDSA
1583 # and classSchema in s!objectClass.
1585 # Let D be the sequence of objects o in the site of the local
1586 # DC such that o!objectCategory = s. D is sorted in ascending
1587 # order by objectGUID.
1589 # Which is a fancy way of saying "sort all the nTDSDSA objects
1590 # in the site by guid in ascending order". Place sorted list
1592 D_sort
= sorted(self
.rw_dsa_table
.values(), cmp=sort_dsa_by_guid
)
1594 # double word number of 100 nanosecond intervals since 1600s
1596 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1597 # if o!interSiteTopologyFailover is 0 or has no value.
1599 # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1600 # so it appears we have to turn f into the same interval
1602 # interSiteTopologyFailover (if set) appears to be in minutes
1603 # so we'll need to convert to senconds and then 100 nanosecond
1605 # XXX [MS-ADTS] 6.2.2.3.1 says it is seconds, not minutes.
1607 # 10,000,000 is number of 100 nanosecond intervals in a second
1608 if self
.site_topo_failover
== 0:
1609 f
= 2 * 60 * 60 * 10000000
1611 f
= self
.site_topo_failover
* 60 * 10000000
1613 # Let o be the site settings object for the site of the local
1614 # DC, or NULL if no such o exists.
1615 d_dsa
= self
.dsa_table
.get(self
.site_topo_generator
)
1617 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1618 # If o != NULL and o!interSiteTopologyGenerator is not the
1619 # nTDSDSA object for the local DC and
1620 # o!interSiteTopologyGenerator is an element dj of sequence D:
1622 if d_dsa
is not None and d_dsa
is not mydsa
:
1623 # From MS-ADTS 6.2.2.3.1 ISTG Selection:
1624 # Let c be the cursor in the replUpToDateVector variable
1625 # associated with the NC replica of the config NC such
1626 # that c.uuidDsa = dj!invocationId. If no such c exists
1627 # (No evidence of replication from current ITSG):
1631 # Else if the current time < c.timeLastSyncSuccess - f
1632 # (Evidence of time sync problem on current ISTG):
1636 # Else (Evidence of replication from current ITSG):
1638 # Let t = c.timeLastSyncSuccess.
1640 # last_success appears to be a double word containing
1641 # number of 100 nanosecond intervals since the 1600s
1642 j_idx
= D_sort
.index(d_dsa
)
1645 for cursor
in c_rep
.rep_replUpToDateVector_cursors
:
1646 if d_dsa
.dsa_ivid
== cursor
.source_dsa_invocation_id
:
1654 #XXX doc says current time < c.timeLastSyncSuccess - f
1655 # which is true only if f is negative or clocks are wrong.
1656 # f is not negative in the default case (2 hours).
1657 elif self
.nt_now
- cursor
.last_sync_success
> f
:
1662 t_time
= cursor
.last_sync_success
1664 # Otherwise (Nominate local DC as ISTG):
1665 # Let i be the integer such that di is the nTDSDSA
1666 # object for the local DC.
1667 # Let t = the current time.
1669 i_idx
= D_sort
.index(mydsa
)
1670 t_time
= self
.nt_now
1672 # Compute a function that maintains the current ISTG if
1673 # it is alive, cycles through other candidates if not.
1675 # Let k be the integer (i + ((current time - t) /
1676 # o!interSiteTopologyFailover)) MOD |D|.
1678 # Note: We don't want to divide by zero here so they must
1679 # have meant "f" instead of "o!interSiteTopologyFailover"
1680 k_idx
= (i_idx
+ ((self
.nt_now
- t_time
) / f
)) % len(D_sort
)
1682 # The local writable DC acts as an ISTG for its site if and
1683 # only if dk is the nTDSDSA object for the local DC. If the
1684 # local DC does not act as an ISTG, the KCC skips the
1685 # remainder of this task.
1686 d_dsa
= D_sort
[k_idx
]
1687 d_dsa
.dsa_is_istg
= True
1689 # Update if we are the ISTG, otherwise return
1690 if d_dsa
is not mydsa
:
1694 if self
.site_topo_generator
== mydsa
.dsa_dnstr
:
1697 self
.site_topo_generator
= mydsa
.dsa_dnstr
1699 # If readonly database then do not perform a
1704 # Perform update to the samdb
1705 ssdn
= "CN=NTDS Site Settings,%s" % self
.site_dnstr
1708 m
.dn
= ldb
.Dn(samdb
, ssdn
)
1710 m
["interSiteTopologyGenerator"] = \
1711 ldb
.MessageElement(mydsa
.dsa_dnstr
, ldb
.FLAG_MOD_REPLACE
,
1712 "interSiteTopologyGenerator")
1716 except ldb
.LdbError
, estr
:
1718 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1722 def is_intrasite_topology_disabled(self
):
1723 '''Returns True if intra-site topology is disabled for site'''
1724 return (self
.site_options
&
1725 dsdb
.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED
) != 0
1727 def is_intersite_topology_disabled(self
):
1728 '''Returns True if inter-site topology is disabled for site'''
1729 return ((self
.site_options
&
1730 dsdb
.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED
)
1733 def is_random_bridgehead_disabled(self
):
1734 '''Returns True if selection of random bridgehead is disabled'''
1735 return (self
.site_options
&
1736 dsdb
.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED
) != 0
1738 def is_detect_stale_disabled(self
):
1739 '''Returns True if detect stale is disabled for site'''
1740 return (self
.site_options
&
1741 dsdb
.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED
) != 0
1743 def is_cleanup_ntdsconn_disabled(self
):
1744 '''Returns True if NTDS Connection cleanup is disabled for site'''
1745 return (self
.site_options
&
1746 dsdb
.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED
) != 0
1748 def same_site(self
, dsa
):
1749 '''Return True if dsa is in this site'''
1750 if self
.get_dsa(dsa
.dsa_dnstr
):
1754 def is_rodc_site(self
):
1755 if len(self
.dsa_table
) > 0 and len(self
.rw_dsa_table
) == 0:
1760 '''Debug dump string output of class'''
1761 text
= "%s:" % self
.__class
__.__name
__
1762 text
= text
+ "\n\tdn=%s" % self
.site_dnstr
1763 text
= text
+ "\n\toptions=0x%X" % self
.site_options
1764 text
= text
+ "\n\ttopo_generator=%s" % self
.site_topo_generator
1765 text
= text
+ "\n\ttopo_failover=%d" % self
.site_topo_failover
1766 for key
, dsa
in self
.dsa_table
.items():
1767 text
= text
+ "\n%s" % dsa
1771 class GraphNode(object):
1772 """A graph node describing a set of edges that should be directed to it.
1774 Each edge is a connection for a particular naming context replica directed
1775 from another node in the forest to this node.
1778 def __init__(self
, dsa_dnstr
, max_node_edges
):
1779 """Instantiate the graph node according to a DSA dn string
1781 :param max_node_edges: maximum number of edges that should ever
1782 be directed to the node
1784 self
.max_edges
= max_node_edges
1785 self
.dsa_dnstr
= dsa_dnstr
1789 text
= "%s:" % self
.__class
__.__name
__
1790 text
= text
+ "\n\tdsa_dnstr=%s" % self
.dsa_dnstr
1791 text
= text
+ "\n\tmax_edges=%d" % self
.max_edges
1793 for i
, edge
in enumerate(self
.edge_from
):
1794 text
= text
+ "\n\tedge_from[%d]=%s" % (i
, edge
)
1797 def add_edge_from(self
, from_dsa_dnstr
):
1798 """Add an edge from the dsa to our graph nodes edge from list
1800 :param from_dsa_dnstr: the dsa that the edge emanates from
1802 assert from_dsa_dnstr
is not None
1804 # No edges from myself to myself
1805 if from_dsa_dnstr
== self
.dsa_dnstr
:
1807 # Only one edge from a particular node
1808 if from_dsa_dnstr
in self
.edge_from
:
1810 # Not too many edges
1811 if len(self
.edge_from
) >= self
.max_edges
:
1813 self
.edge_from
.append(from_dsa_dnstr
)
1816 def add_edges_from_connections(self
, dsa
):
1817 """For each nTDSConnection object associated with a particular
1818 DSA, we test if it implies an edge to this graph node (i.e.
1819 the "fromServer" attribute). If it does then we add an
1820 edge from the server unless we are over the max edges for this
1823 :param dsa: dsa with a dnstr equivalent to his graph node
1825 for connect
in dsa
.connect_table
.values():
1826 self
.add_edge_from(connect
.from_dnstr
)
1828 def add_connections_from_edges(self
, dsa
, transport
):
1829 """For each edge directed to this graph node, ensure there
1830 is a corresponding nTDSConnection object in the dsa.
1832 for edge_dnstr
in self
.edge_from
:
1833 connections
= dsa
.get_connection_by_from_dnstr(edge_dnstr
)
1835 # For each edge directed to the NC replica that
1836 # "should be present" on the local DC, the KCC determines
1837 # whether an object c exists such that:
1839 # c is a child of the DC's nTDSDSA object.
1840 # c.objectCategory = nTDSConnection
1842 # Given the NC replica ri from which the edge is directed,
1843 # c.fromServer is the dsname of the nTDSDSA object of
1844 # the DC on which ri "is present".
1846 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1849 for connect
in connections
:
1850 if connect
.is_rodc_topology():
1857 # if no such object exists then the KCC adds an object
1858 # c with the following attributes
1860 # Generate a new dnstr for this nTDSConnection
1861 opt
= dsdb
.NTDSCONN_OPT_IS_GENERATED
1862 flags
= (dsdb
.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1863 dsdb
.SYSTEM_FLAG_CONFIG_ALLOW_MOVE
)
1865 dsa
.new_connection(opt
, flags
, transport
, edge_dnstr
, None)
1867 def has_sufficient_edges(self
):
1868 '''Return True if we have met the maximum "from edges" criteria'''
1869 if len(self
.edge_from
) >= self
.max_edges
:
1874 class Transport(object):
1875 """Class defines a Inter-site transport found under Sites
1878 def __init__(self
, dnstr
):
1883 self
.address_attr
= None
1884 self
.bridgehead_list
= []
1887 '''Debug dump string output of Transport object'''
1889 text
= "%s:\n\tdn=%s" % (self
.__class
__.__name
__, self
.dnstr
)
1890 text
= text
+ "\n\tguid=%s" % str(self
.guid
)
1891 text
= text
+ "\n\toptions=%d" % self
.options
1892 text
= text
+ "\n\taddress_attr=%s" % self
.address_attr
1893 text
= text
+ "\n\tname=%s" % self
.name
1894 for dnstr
in self
.bridgehead_list
:
1895 text
= text
+ "\n\tbridgehead_list=%s" % dnstr
1899 def load_transport(self
, samdb
):
1900 """Given a Transport object with an prior initialization
1901 for the object's DN, search for the DN and load attributes
1904 attrs
= ["objectGUID",
1907 "bridgeheadServerListBL",
1908 "transportAddressAttribute"]
1910 res
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
,
1913 except ldb
.LdbError
, (enum
, estr
):
1914 raise KCCError("Unable to find Transport for (%s) - (%s)" %
1918 self
.guid
= misc
.GUID(samdb
.schema_format_value("objectGUID",
1919 msg
["objectGUID"][0]))
1921 if "options" in msg
:
1922 self
.options
= int(msg
["options"][0])
1924 if "transportAddressAttribute" in msg
:
1925 self
.address_attr
= str(msg
["transportAddressAttribute"][0])
1928 self
.name
= str(msg
["name"][0])
1930 if "bridgeheadServerListBL" in msg
:
1931 for value
in msg
["bridgeheadServerListBL"]:
1932 dsdn
= dsdb_Dn(samdb
, value
)
1933 dnstr
= str(dsdn
.dn
)
1934 if dnstr
not in self
.bridgehead_list
:
1935 self
.bridgehead_list
.append(dnstr
)
1938 class RepsFromTo(object):
1939 """Class encapsulation of the NDR repsFromToBlob.
1941 Removes the necessity of external code having to
1942 understand about other_info or manipulation of
1945 def __init__(self
, nc_dnstr
=None, ndr_blob
=None):
1947 self
.__dict
__['to_be_deleted'] = False
1948 self
.__dict
__['nc_dnstr'] = nc_dnstr
1949 self
.__dict
__['update_flags'] = 0x0
1950 # XXX the following sounds dubious and/or better solved
1951 # elsewhere, but lets leave it for now. In particular, there
1952 # seems to be no reason for all the non-ndr generated
1953 # attributes to be handled in the round about way (e.g.
1954 # self.__dict__['to_be_deleted'] = False above). On the other
1955 # hand, it all seems to work. Hooray! Hands off!.
1959 # There is a very subtle bug here with python
1960 # and our NDR code. If you assign directly to
1961 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1962 # then a proper python GC reference count is not
1965 # To work around this we maintain an internal
1966 # reference to "dns_name(x)" and "other_info" elements
1967 # of repsFromToBlob. This internal reference
1968 # is hidden within this class but it is why you
1969 # see statements like this below:
1971 # self.__dict__['ndr_blob'].ctr.other_info = \
1972 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1974 # That would appear to be a redundant assignment but
1975 # it is necessary to hold a proper python GC reference
1977 if ndr_blob
is None:
1978 self
.__dict
__['ndr_blob'] = drsblobs
.repsFromToBlob()
1979 self
.__dict
__['ndr_blob'].version
= 0x1
1980 self
.__dict
__['dns_name1'] = None
1981 self
.__dict
__['dns_name2'] = None
1983 self
.__dict
__['ndr_blob'].ctr
.other_info
= \
1984 self
.__dict
__['other_info'] = drsblobs
.repsFromTo1OtherInfo()
1987 self
.__dict
__['ndr_blob'] = ndr_blob
1988 self
.__dict
__['other_info'] = ndr_blob
.ctr
.other_info
1990 if ndr_blob
.version
== 0x1:
1991 self
.__dict
__['dns_name1'] = ndr_blob
.ctr
.other_info
.dns_name
1992 self
.__dict
__['dns_name2'] = None
1994 self
.__dict
__['dns_name1'] = ndr_blob
.ctr
.other_info
.dns_name1
1995 self
.__dict
__['dns_name2'] = ndr_blob
.ctr
.other_info
.dns_name2
1998 '''Debug dump string output of class'''
2000 text
= "%s:" % self
.__class
__.__name
__
2001 text
+= "\n\tdnstr=%s" % self
.nc_dnstr
2002 text
+= "\n\tupdate_flags=0x%X" % self
.update_flags
2003 text
+= "\n\tversion=%d" % self
.version
2004 text
+= "\n\tsource_dsa_obj_guid=%s" % self
.source_dsa_obj_guid
2005 text
+= ("\n\tsource_dsa_invocation_id=%s" %
2006 self
.source_dsa_invocation_id
)
2007 text
+= "\n\ttransport_guid=%s" % self
.transport_guid
2008 text
+= "\n\treplica_flags=0x%X" % self
.replica_flags
2009 text
+= ("\n\tconsecutive_sync_failures=%d" %
2010 self
.consecutive_sync_failures
)
2011 text
+= "\n\tlast_success=%s" % self
.last_success
2012 text
+= "\n\tlast_attempt=%s" % self
.last_attempt
2013 text
+= "\n\tdns_name1=%s" % self
.dns_name1
2014 text
+= "\n\tdns_name2=%s" % self
.dns_name2
2015 text
+= "\n\tschedule[ "
2016 for slot
in self
.schedule
:
2017 text
+= "0x%X " % slot
2022 def __setattr__(self
, item
, value
):
2023 """Set an attribute and chyange update flag.
2025 Be aware that setting any RepsFromTo attribute will set the
2026 drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS update flag.
2028 if item
in ['schedule', 'replica_flags', 'transport_guid',
2029 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2030 'consecutive_sync_failures', 'last_success',
2033 if item
in ['replica_flags']:
2034 self
.__dict
__['update_flags'] |
= drsuapi
.DRSUAPI_DRS_UPDATE_FLAGS
2035 elif item
in ['schedule']:
2036 self
.__dict
__['update_flags'] |
= drsuapi
.DRSUAPI_DRS_UPDATE_SCHEDULE
2038 setattr(self
.__dict
__['ndr_blob'].ctr
, item
, value
)
2040 elif item
in ['dns_name1']:
2041 self
.__dict
__['dns_name1'] = value
2043 if self
.__dict
__['ndr_blob'].version
== 0x1:
2044 self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name
= \
2045 self
.__dict
__['dns_name1']
2047 self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name1
= \
2048 self
.__dict
__['dns_name1']
2050 elif item
in ['dns_name2']:
2051 self
.__dict
__['dns_name2'] = value
2053 if self
.__dict
__['ndr_blob'].version
== 0x1:
2054 raise AttributeError(item
)
2056 self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name2
= \
2057 self
.__dict
__['dns_name2']
2059 elif item
in ['nc_dnstr']:
2060 self
.__dict
__['nc_dnstr'] = value
2062 elif item
in ['to_be_deleted']:
2063 self
.__dict
__['to_be_deleted'] = value
2065 elif item
in ['version']:
2066 raise AttributeError("Attempt to set readonly attribute %s" % item
)
2068 raise AttributeError("Unknown attribute %s" % item
)
2070 self
.__dict
__['update_flags'] |
= drsuapi
.DRSUAPI_DRS_UPDATE_ADDRESS
2072 def __getattr__(self
, item
):
2073 """Overload of RepsFromTo attribute retrieval.
2075 Allows external code to ignore substructures within the blob
2077 if item
in ['schedule', 'replica_flags', 'transport_guid',
2078 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2079 'consecutive_sync_failures', 'last_success',
2081 return getattr(self
.__dict
__['ndr_blob'].ctr
, item
)
2083 elif item
in ['version']:
2084 return self
.__dict
__['ndr_blob'].version
2086 elif item
in ['dns_name1']:
2087 if self
.__dict
__['ndr_blob'].version
== 0x1:
2088 return self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name
2090 return self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name1
2092 elif item
in ['dns_name2']:
2093 if self
.__dict
__['ndr_blob'].version
== 0x1:
2094 raise AttributeError(item
)
2096 return self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name2
2098 elif item
in ['to_be_deleted']:
2099 return self
.__dict
__['to_be_deleted']
2101 elif item
in ['nc_dnstr']:
2102 return self
.__dict
__['nc_dnstr']
2104 elif item
in ['update_flags']:
2105 return self
.__dict
__['update_flags']
2107 raise AttributeError("Unknown attribute %s" % item
)
2109 def is_modified(self
):
2110 return (self
.update_flags
!= 0x0)
2112 def set_unmodified(self
):
2113 self
.__dict
__['update_flags'] = 0x0
2116 class SiteLink(object):
2117 """Class defines a site link found under sites
2120 def __init__(self
, dnstr
):
2123 self
.system_flags
= 0
2125 self
.schedule
= None
2126 self
.interval
= None
2130 '''Debug dump string output of Transport object'''
2132 text
= "%s:\n\tdn=%s" % (self
.__class
__.__name
__, self
.dnstr
)
2133 text
= text
+ "\n\toptions=%d" % self
.options
2134 text
= text
+ "\n\tsystem_flags=%d" % self
.system_flags
2135 text
= text
+ "\n\tcost=%d" % self
.cost
2136 text
= text
+ "\n\tinterval=%s" % self
.interval
2138 if self
.schedule
is not None:
2139 text
+= "\n\tschedule.size=%s" % self
.schedule
.size
2140 text
+= "\n\tschedule.bandwidth=%s" % self
.schedule
.bandwidth
2141 text
+= ("\n\tschedule.numberOfSchedules=%s" %
2142 self
.schedule
.numberOfSchedules
)
2144 for i
, header
in enumerate(self
.schedule
.headerArray
):
2145 text
+= ("\n\tschedule.headerArray[%d].type=%d" %
2147 text
+= ("\n\tschedule.headerArray[%d].offset=%d" %
2149 text
= text
+ "\n\tschedule.dataArray[%d].slots[ " % i
2150 for slot
in self
.schedule
.dataArray
[i
].slots
:
2151 text
= text
+ "0x%X " % slot
2154 for dnstr
in self
.site_list
:
2155 text
= text
+ "\n\tsite_list=%s" % dnstr
2158 def load_sitelink(self
, samdb
):
2159 """Given a siteLink object with an prior initialization
2160 for the object's DN, search for the DN and load attributes
2170 res
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
,
2171 attrs
=attrs
, controls
=['extended_dn:0'])
2173 except ldb
.LdbError
, (enum
, estr
):
2174 raise KCCError("Unable to find SiteLink for (%s) - (%s)" %
2179 if "options" in msg
:
2180 self
.options
= int(msg
["options"][0])
2182 if "systemFlags" in msg
:
2183 self
.system_flags
= int(msg
["systemFlags"][0])
2186 self
.cost
= int(msg
["cost"][0])
2188 if "replInterval" in msg
:
2189 self
.interval
= int(msg
["replInterval"][0])
2191 if "siteList" in msg
:
2192 for value
in msg
["siteList"]:
2193 dsdn
= dsdb_Dn(samdb
, value
)
2194 guid
= misc
.GUID(dsdn
.dn
.get_extended_component('GUID'))
2195 if guid
not in self
.site_list
:
2196 self
.site_list
.append(guid
)
2198 if "schedule" in msg
:
2199 self
.schedule
= ndr_unpack(drsblobs
.schedule
, value
)
2201 self
.schedule
= new_connection_schedule()
2204 class KCCFailedObject(object):
2205 def __init__(self
, uuid
, failure_count
, time_first_failure
,
2206 last_result
, dns_name
):
2208 self
.failure_count
= failure_count
2209 self
.time_first_failure
= time_first_failure
2210 self
.last_result
= last_result
2211 self
.dns_name
= dns_name
2214 ##################################################
2215 # Global Functions and Variables
2216 ##################################################
2218 def get_dsa_config_rep(dsa
):
2219 # Find configuration NC replica for the DSA
2220 for c_rep
in dsa
.current_rep_table
.values():
2221 if c_rep
.is_config():
2224 raise KCCError("Unable to find config NC replica for (%s)" %
2228 def sort_dsa_by_guid(dsa1
, dsa2
):
2229 "use ndr_pack for GUID comparison, as appears correct in some places"""
2230 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
2233 def new_connection_schedule():
2234 """Create a default schedule for an NTDSConnection or Sitelink. This
2235 is packed differently from the repltimes schedule used elsewhere
2236 in KCC (where the 168 nibbles are packed into 84 bytes).
2238 # 168 byte instances of the 0x01 value. The low order 4 bits
2239 # of the byte equate to 15 minute intervals within a single hour.
2240 # There are 168 bytes because there are 168 hours in a full week
2241 # Effectively we are saying to perform replication at the end of
2242 # each hour of the week
2243 schedule = drsblobs.schedule()
2246 schedule.bandwidth = 0
2247 schedule.numberOfSchedules = 1
2249 header = drsblobs.scheduleHeader()
2253 schedule.headerArray = [header]
2255 data = drsblobs.scheduleSlots()
2256 data.slots = [0x01] * 168
2258 schedule.dataArray = [data]