1 # KCC topology utilities
3 # Copyright (C) Dave Craft 2011
4 # Copyright (C) Jelmer Vernooij 2011
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
23 from samba
import dsdb
, unix2nttime
24 from samba
.dcerpc
import (
29 from samba
.common
import dsdb_Dn
30 from samba
.ndr
import (ndr_unpack
, ndr_pack
)
34 (unknown
, schema
, domain
, config
, application
) = range(0, 5)
37 class NamingContext(object):
38 """Base class for a naming context.
40 Holds the DN, GUID, SID (if available) and type of the DN.
41 Subclasses may inherit from this and specialize
44 def __init__(self
, nc_dnstr
):
45 """Instantiate a NamingContext
47 :param nc_dnstr: NC dn string
49 self
.nc_dnstr
= nc_dnstr
52 self
.nc_type
= NCType
.unknown
55 '''Debug dump string output of class'''
56 text
= "%s:" % self
.__class
__.__name
__
57 text
= text
+ "\n\tnc_dnstr=%s" % self
.nc_dnstr
58 text
= text
+ "\n\tnc_guid=%s" % str(self
.nc_guid
)
60 if self
.nc_sid
is None:
61 text
= text
+ "\n\tnc_sid=<absent>"
63 text
= text
+ "\n\tnc_sid=<present>"
65 text
= text
+ "\n\tnc_type=%s" % self
.nc_type
68 def load_nc(self
, samdb
):
69 attrs
= [ "objectGUID",
72 res
= samdb
.search(base
=self
.nc_dnstr
,
73 scope
=ldb
.SCOPE_BASE
, attrs
=attrs
)
75 except ldb
.LdbError
, (enum
, estr
):
76 raise Exception("Unable to find naming context (%s)" %
77 (self
.nc_dnstr
, estr
))
79 if "objectGUID" in msg
:
80 self
.nc_guid
= misc
.GUID(samdb
.schema_format_value("objectGUID",
81 msg
["objectGUID"][0]))
82 if "objectSid" in msg
:
83 self
.nc_sid
= msg
["objectSid"][0]
85 assert self
.nc_guid
is not None
88 '''Return True if NC is schema'''
89 assert self
.nc_type
!= NCType
.unknown
90 return self
.nc_type
== NCType
.schema
93 '''Return True if NC is domain'''
94 assert self
.nc_type
!= NCType
.unknown
95 return self
.nc_type
== NCType
.domain
97 def is_application(self
):
98 '''Return True if NC is application'''
99 assert self
.nc_type
!= NCType
.unknown
100 return self
.nc_type
== NCType
.application
103 '''Return True if NC is config'''
104 assert self
.nc_type
!= NCType
.unknown
105 return self
.nc_type
== NCType
.config
107 def identify_by_basedn(self
, samdb
):
108 """Given an NC object, identify what type is is thru
109 the samdb basedn strings and NC sid value
111 # Invoke loader to initialize guid and more
112 # importantly sid value (sid is used to identify
114 if self
.nc_guid
is None:
117 # We check against schema and config because they
118 # will be the same for all nTDSDSAs in the forest.
119 # That leaves the domain NCs which can be identified
120 # by sid and application NCs as the last identified
121 if self
.nc_dnstr
== str(samdb
.get_schema_basedn()):
122 self
.nc_type
= NCType
.schema
123 elif self
.nc_dnstr
== str(samdb
.get_config_basedn()):
124 self
.nc_type
= NCType
.config
125 elif self
.nc_sid
is not None:
126 self
.nc_type
= NCType
.domain
128 self
.nc_type
= NCType
.application
130 def identify_by_dsa_attr(self
, samdb
, attr
):
131 """Given an NC which has been discovered thru the
132 nTDSDSA database object, determine what type of NC
133 it is (i.e. schema, config, domain, application) via
134 the use of the schema attribute under which the NC
137 :param attr: attr of nTDSDSA object where NC DN appears
139 # If the NC is listed under msDS-HasDomainNCs then
140 # this can only be a domain NC and it is our default
141 # domain for this dsa
142 if attr
== "msDS-HasDomainNCs":
143 self
.nc_type
= NCType
.domain
145 # If the NC is listed under hasPartialReplicaNCs
146 # this is only a domain NC
147 elif attr
== "hasPartialReplicaNCs":
148 self
.nc_type
= NCType
.domain
150 # NCs listed under hasMasterNCs are either
151 # default domain, schema, or config. We
152 # utilize the identify_by_basedn() to
154 elif attr
== "hasMasterNCs":
155 self
.identify_by_basedn(samdb
)
157 # Still unknown (unlikely) but for completeness
158 # and for finally identifying application NCs
159 if self
.nc_type
== NCType
.unknown
:
160 self
.identify_by_basedn(samdb
)
163 class NCReplica(NamingContext
):
164 """Naming context replica that is relative to a specific DSA.
166 This is a more specific form of NamingContext class (inheriting from that
167 class) and it identifies unique attributes of the DSA's replica for a NC.
170 def __init__(self
, dsa_dnstr
, dsa_guid
, nc_dnstr
):
171 """Instantiate a Naming Context Replica
173 :param dsa_guid: GUID of DSA where replica appears
174 :param nc_dnstr: NC dn string
176 self
.rep_dsa_dnstr
= dsa_dnstr
177 self
.rep_dsa_guid
= dsa_guid
178 self
.rep_default
= False # replica for DSA's default domain
179 self
.rep_partial
= False
181 self
.rep_instantiated_flags
= 0
183 self
.rep_fsmo_role_owner
= None
186 self
.rep_repsFrom
= []
188 # The (is present) test is a combination of being
189 # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or
190 # hasPartialReplicaNCs) as well as its replica flags found
191 # thru the msDS-HasInstantiatedNCs. If the NC replica meets
192 # the first enumeration test then this flag is set true
193 self
.rep_present_criteria_one
= False
195 # Call my super class we inherited from
196 NamingContext
.__init
__(self
, nc_dnstr
)
199 '''Debug dump string output of class'''
200 text
= "%s:" % self
.__class
__.__name
__
201 text
= text
+ "\n\tdsa_dnstr=%s" % self
.rep_dsa_dnstr
202 text
= text
+ "\n\tdsa_guid=%s" % str(self
.rep_dsa_guid
)
203 text
= text
+ "\n\tdefault=%s" % self
.rep_default
204 text
= text
+ "\n\tro=%s" % self
.rep_ro
205 text
= text
+ "\n\tpartial=%s" % self
.rep_partial
206 text
= text
+ "\n\tpresent=%s" % self
.is_present()
207 text
= text
+ "\n\tfsmo_role_owner=%s" % self
.rep_fsmo_role_owner
209 for rep
in self
.rep_repsFrom
:
210 text
= text
+ "\n%s" % rep
212 return "%s\n%s" % (NamingContext
.__str
__(self
), text
)
214 def set_instantiated_flags(self
, flags
=None):
215 '''Set or clear NC replica instantiated flags'''
217 self
.rep_instantiated_flags
= 0
219 self
.rep_instantiated_flags
= flags
221 def identify_by_dsa_attr(self
, samdb
, attr
):
222 """Given an NC which has been discovered thru the
223 nTDSDSA database object, determine what type of NC
224 replica it is (i.e. partial, read only, default)
226 :param attr: attr of nTDSDSA object where NC DN appears
228 # If the NC was found under hasPartialReplicaNCs
229 # then a partial replica at this dsa
230 if attr
== "hasPartialReplicaNCs":
231 self
.rep_partial
= True
232 self
.rep_present_criteria_one
= True
234 # If the NC is listed under msDS-HasDomainNCs then
235 # this can only be a domain NC and it is the DSA's
237 elif attr
== "msDS-HasDomainNCs":
238 self
.rep_default
= True
240 # NCs listed under hasMasterNCs are either
241 # default domain, schema, or config. We check
242 # against schema and config because they will be
243 # the same for all nTDSDSAs in the forest. That
244 # leaves the default domain NC remaining which
245 # may be different for each nTDSDSAs (and thus
246 # we don't compare agains this samdb's default
248 elif attr
== "hasMasterNCs":
249 self
.rep_present_criteria_one
= True
251 if self
.nc_dnstr
!= str(samdb
.get_schema_basedn()) and \
252 self
.nc_dnstr
!= str(samdb
.get_config_basedn()):
253 self
.rep_default
= True
256 elif attr
== "msDS-hasFullReplicaNCs":
257 self
.rep_present_criteria_one
= True
261 elif attr
== "msDS-hasMasterNCs":
264 # Now use this DSA attribute to identify the naming
265 # context type by calling the super class method
267 NamingContext
.identify_by_dsa_attr(self
, samdb
, attr
)
269 def is_default(self
):
270 """Whether this is a default domain for the dsa that this NC appears on
272 return self
.rep_default
275 '''Return True if NC replica is read only'''
278 def is_partial(self
):
279 '''Return True if NC replica is partial'''
280 return self
.rep_partial
282 def is_present(self
):
283 """Given an NC replica which has been discovered thru the
284 nTDSDSA database object and populated with replica flags
285 from the msDS-HasInstantiatedNCs; return whether the NC
286 replica is present (true) or if the IT_NC_GOING flag is
287 set then the NC replica is not present (false)
289 if self
.rep_present_criteria_one
and \
290 self
.rep_instantiated_flags
& dsdb
.INSTANCE_TYPE_NC_GOING
== 0:
294 def load_repsFrom(self
, samdb
):
295 """Given an NC replica which has been discovered thru the nTDSDSA
296 database object, load the repsFrom attribute for the local replica.
297 held by my dsa. The repsFrom attribute is not replicated so this
298 attribute is relative only to the local DSA that the samdb exists on
301 res
= samdb
.search(base
=self
.nc_dnstr
, scope
=ldb
.SCOPE_BASE
,
302 attrs
=[ "repsFrom" ])
304 except ldb
.LdbError
, (enum
, estr
):
305 raise Exception("Unable to find NC for (%s) - (%s)" %
306 (self
.nc_dnstr
, estr
))
310 # Possibly no repsFrom if this is a singleton DC
311 if "repsFrom" in msg
:
312 for value
in msg
["repsFrom"]:
313 rep
= RepsFromTo(self
.nc_dnstr
,
314 ndr_unpack(drsblobs
.repsFromToBlob
, value
))
315 self
.rep_repsFrom
.append(rep
)
317 def commit_repsFrom(self
, samdb
, ro
=False):
318 """Commit repsFrom to the database"""
320 # XXX - This is not truly correct according to the MS-TECH
321 # docs. To commit a repsFrom we should be using RPCs
322 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
323 # IDL_DRSReplicaDel to affect a repsFrom change.
325 # Those RPCs are missing in samba, so I'll have to
326 # implement them to get this to more accurately
327 # reflect the reference docs. As of right now this
328 # commit to the database will work as its what the
334 for repsFrom
in self
.rep_repsFrom
:
336 # Leave out any to be deleted from
337 # replacement list. Build a list
338 # of to be deleted reps which we will
339 # remove from rep_repsFrom list below
340 if repsFrom
.to_be_deleted
:
341 delreps
.append(repsFrom
)
345 if repsFrom
.is_modified():
346 repsFrom
.set_unmodified()
349 # current (unmodified) elements also get
350 # appended here but no changes will occur
351 # unless something is "to be modified" or
353 newreps
.append(ndr_pack(repsFrom
.ndr_blob
))
355 # Now delete these from our list of rep_repsFrom
356 for repsFrom
in delreps
:
357 self
.rep_repsFrom
.remove(repsFrom
)
360 # Nothing to do if no reps have been modified or
361 # need to be deleted or input option has informed
362 # us to be "readonly" (ro). Leave database
368 m
.dn
= ldb
.Dn(samdb
, self
.nc_dnstr
)
371 ldb
.MessageElement(newreps
, ldb
.FLAG_MOD_REPLACE
, "repsFrom")
376 except ldb
.LdbError
, estr
:
377 raise Exception("Could not set repsFrom for (%s) - (%s)" %
378 (self
.dsa_dnstr
, estr
))
380 def dumpstr_to_be_deleted(self
):
382 for repsFrom
in self
.rep_repsFrom
:
383 if repsFrom
.to_be_deleted
:
385 text
= text
+ "\n%s" % repsFrom
387 text
= "%s" % repsFrom
390 def dumpstr_to_be_modified(self
):
392 for repsFrom
in self
.rep_repsFrom
:
393 if repsFrom
.is_modified():
395 text
= text
+ "\n%s" % repsFrom
397 text
= "%s" % repsFrom
400 def load_fsmo_roles(self
, samdb
):
401 """Given an NC replica which has been discovered thru the nTDSDSA
402 database object, load the fSMORoleOwner attribute.
405 res
= samdb
.search(base
=self
.nc_dnstr
, scope
=ldb
.SCOPE_BASE
,
406 attrs
=[ "fSMORoleOwner" ])
408 except ldb
.LdbError
, (enum
, estr
):
409 raise Exception("Unable to find NC for (%s) - (%s)" %
410 (self
.nc_dnstr
, estr
))
414 # Possibly no fSMORoleOwner
415 if "fSMORoleOwner" in msg
:
416 self
.rep_fsmo_role_owner
= msg
["fSMORoleOwner"]
418 def is_fsmo_role_owner(self
, dsa_dnstr
):
419 if self
.rep_fsmo_role_owner
is not None and \
420 self
.rep_fsmo_role_owner
== dsa_dnstr
:
425 class DirectoryServiceAgent(object):
427 def __init__(self
, dsa_dnstr
):
428 """Initialize DSA class.
430 Class is subsequently fully populated by calling the load_dsa() method
432 :param dsa_dnstr: DN of the nTDSDSA
434 self
.dsa_dnstr
= dsa_dnstr
437 self
.dsa_is_ro
= False
438 self
.dsa_is_istg
= False
440 self
.dsa_behavior
= 0
441 self
.default_dnstr
= None # default domain dn string for dsa
443 # NCReplicas for this dsa that are "present"
444 # Indexed by DN string of naming context
445 self
.current_rep_table
= {}
447 # NCReplicas for this dsa that "should be present"
448 # Indexed by DN string of naming context
449 self
.needed_rep_table
= {}
451 # NTDSConnections for this dsa. These are current
452 # valid connections that are committed or pending a commit
453 # in the database. Indexed by DN string of connection
454 self
.connect_table
= {}
457 '''Debug dump string output of class'''
459 text
= "%s:" % self
.__class
__.__name
__
460 if self
.dsa_dnstr
is not None:
461 text
= text
+ "\n\tdsa_dnstr=%s" % self
.dsa_dnstr
462 if self
.dsa_guid
is not None:
463 text
= text
+ "\n\tdsa_guid=%s" % str(self
.dsa_guid
)
464 if self
.dsa_ivid
is not None:
465 text
= text
+ "\n\tdsa_ivid=%s" % str(self
.dsa_ivid
)
467 text
= text
+ "\n\tro=%s" % self
.is_ro()
468 text
= text
+ "\n\tgc=%s" % self
.is_gc()
469 text
= text
+ "\n\tistg=%s" % self
.is_istg()
471 text
= text
+ "\ncurrent_replica_table:"
472 text
= text
+ "\n%s" % self
.dumpstr_current_replica_table()
473 text
= text
+ "\nneeded_replica_table:"
474 text
= text
+ "\n%s" % self
.dumpstr_needed_replica_table()
475 text
= text
+ "\nconnect_table:"
476 text
= text
+ "\n%s" % self
.dumpstr_connect_table()
480 def get_current_replica(self
, nc_dnstr
):
481 if nc_dnstr
in self
.current_rep_table
.keys():
482 return self
.current_rep_table
[nc_dnstr
]
487 '''Returns True if dsa is intersite topology generator for it's site'''
488 # The KCC on an RODC always acts as an ISTG for itself
489 return self
.dsa_is_istg
or self
.dsa_is_ro
492 '''Returns True if dsa a read only domain controller'''
493 return self
.dsa_is_ro
496 '''Returns True if dsa hosts a global catalog'''
497 if (self
.options
& dsdb
.DS_NTDSDSA_OPT_IS_GC
) != 0:
501 def is_minimum_behavior(self
, version
):
502 """Is dsa at minimum windows level greater than or equal to (version)
504 :param version: Windows version to test against
505 (e.g. DS_BEHAVIOR_WIN2008)
507 if self
.dsa_behavior
>= version
:
511 def is_translate_ntdsconn_disabled(self
):
512 """Whether this allows NTDSConnection translation in its options."""
513 if (self
.options
& dsdb
.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE
) != 0:
517 def get_rep_tables(self
):
518 """Return DSA current and needed replica tables
520 return self
.current_rep_table
, self
.needed_rep_table
522 def get_parent_dnstr(self
):
523 """Get the parent DN string of this object."""
524 head
, sep
, tail
= self
.dsa_dnstr
.partition(',')
527 def load_dsa(self
, samdb
):
528 """Load a DSA from the samdb.
530 Prior initialization has given us the DN of the DSA that we are to
531 load. This method initializes all other attributes, including loading
532 the NC replica table for this DSA.
534 attrs
= ["objectGUID",
538 "msDS-Behavior-Version"]
540 res
= samdb
.search(base
=self
.dsa_dnstr
, scope
=ldb
.SCOPE_BASE
,
543 except ldb
.LdbError
, (enum
, estr
):
544 raise Exception("Unable to find nTDSDSA for (%s) - (%s)" %
545 (self
.dsa_dnstr
, estr
))
548 self
.dsa_guid
= misc
.GUID(samdb
.schema_format_value("objectGUID",
549 msg
["objectGUID"][0]))
551 # RODCs don't originate changes and thus have no invocationId,
552 # therefore we must check for existence first
553 if "invocationId" in msg
:
554 self
.dsa_ivid
= misc
.GUID(samdb
.schema_format_value("objectGUID",
555 msg
["invocationId"][0]))
558 self
.options
= int(msg
["options"][0])
560 if "msDS-isRODC" in msg
and msg
["msDS-isRODC"][0] == "TRUE":
561 self
.dsa_is_ro
= True
563 self
.dsa_is_ro
= False
565 if "msDS-Behavior-Version" in msg
:
566 self
.dsa_behavior
= int(msg
['msDS-Behavior-Version'][0])
568 # Load the NC replicas that are enumerated on this dsa
569 self
.load_current_replica_table(samdb
)
571 # Load the nTDSConnection that are enumerated on this dsa
572 self
.load_connection_table(samdb
)
574 def load_current_replica_table(self
, samdb
):
575 """Method to load the NC replica's listed for DSA object.
577 This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs,
578 hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, and
579 msDS-HasInstantiatedNCs) to determine complete list of NC replicas that
580 are enumerated for the DSA. Once a NC replica is loaded it is
581 identified (schema, config, etc) and the other replica attributes
582 (partial, ro, etc) are determined.
584 :param samdb: database to query for DSA replica list
586 ncattrs
= [ # not RODC - default, config, schema (old style)
588 # not RODC - default, config, schema, app NCs
590 # domain NC partial replicas
591 "hasPartialReplicaNCs",
594 # RODC only - default, config, schema, app NCs
595 "msDS-hasFullReplicaNCs",
596 # Identifies if replica is coming, going, or stable
597 "msDS-HasInstantiatedNCs" ]
599 res
= samdb
.search(base
=self
.dsa_dnstr
, scope
=ldb
.SCOPE_BASE
,
602 except ldb
.LdbError
, (enum
, estr
):
603 raise Exception("Unable to find nTDSDSA NCs for (%s) - (%s)" %
604 (self
.dsa_dnstr
, estr
))
606 # The table of NCs for the dsa we are searching
609 # We should get one response to our query here for
610 # the ntds that we requested
613 # Our response will contain a number of elements including
614 # the dn of the dsa as well as elements for each
615 # attribute (e.g. hasMasterNCs). Each of these elements
616 # is a dictonary list which we retrieve the keys for and
617 # then iterate over them
618 for k
in res
[0].keys():
622 # For each attribute type there will be one or more DNs
623 # listed. For instance DCs normally have 3 hasMasterNCs
625 for value
in res
[0][k
]:
626 # Turn dn into a dsdb_Dn so we can use
627 # its methods to parse a binary DN
628 dsdn
= dsdb_Dn(samdb
, value
)
629 flags
= dsdn
.get_binary_integer()
632 if not dnstr
in tmp_table
.keys():
633 rep
= NCReplica(self
.dsa_dnstr
, self
.dsa_guid
, dnstr
)
634 tmp_table
[dnstr
] = rep
636 rep
= tmp_table
[dnstr
]
638 if k
== "msDS-HasInstantiatedNCs":
639 rep
.set_instantiated_flags(flags
)
642 rep
.identify_by_dsa_attr(samdb
, k
)
644 # if we've identified the default domain NC
645 # then save its DN string
647 self
.default_dnstr
= dnstr
649 raise Exception("No nTDSDSA NCs for (%s)" % self
.dsa_dnstr
)
651 # Assign our newly built NC replica table to this dsa
652 self
.current_rep_table
= tmp_table
654 def add_needed_replica(self
, rep
):
655 """Method to add a NC replica that "should be present" to the
656 needed_rep_table if not already in the table
658 if not rep
.nc_dnstr
in self
.needed_rep_table
.keys():
659 self
.needed_rep_table
[rep
.nc_dnstr
] = rep
661 def load_connection_table(self
, samdb
):
662 """Method to load the nTDSConnections listed for DSA object.
664 :param samdb: database to query for DSA connection list
667 res
= samdb
.search(base
=self
.dsa_dnstr
,
668 scope
=ldb
.SCOPE_SUBTREE
,
669 expression
="(objectClass=nTDSConnection)")
671 except ldb
.LdbError
, (enum
, estr
):
672 raise Exception("Unable to find nTDSConnection for (%s) - (%s)" %
673 (self
.dsa_dnstr
, estr
))
679 if dnstr
in self
.connect_table
.keys():
682 connect
= NTDSConnection(dnstr
)
684 connect
.load_connection(samdb
)
685 self
.connect_table
[dnstr
] = connect
687 def commit_connections(self
, samdb
, ro
=False):
688 """Method to commit any uncommitted nTDSConnections
689 modifications that are in our table. These would be
690 identified connections that are marked to be added or
693 :param samdb: database to commit DSA connection list to
694 :param ro: if (true) then peform internal operations but
695 do not write to the database (readonly)
699 for dnstr
, connect
in self
.connect_table
.items():
700 if connect
.to_be_added
:
701 connect
.commit_added(samdb
, ro
)
703 if connect
.to_be_modified
:
704 connect
.commit_modified(samdb
, ro
)
706 if connect
.to_be_deleted
:
707 connect
.commit_deleted(samdb
, ro
)
708 delconn
.append(dnstr
)
710 # Now delete the connection from the table
711 for dnstr
in delconn
:
712 del self
.connect_table
[dnstr
]
714 def add_connection(self
, dnstr
, connect
):
715 assert dnstr
not in self
.connect_table
.keys()
716 self
.connect_table
[dnstr
] = connect
718 def get_connection_by_from_dnstr(self
, from_dnstr
):
719 """Scan DSA nTDSConnection table and return connection
720 with a "fromServer" dn string equivalent to method
723 :param from_dnstr: search for this from server entry
725 for dnstr
, connect
in self
.connect_table
.items():
726 if connect
.get_from_dnstr() == from_dnstr
:
730 def dumpstr_current_replica_table(self
):
731 '''Debug dump string output of current replica table'''
733 for k
in self
.current_rep_table
.keys():
735 text
= text
+ "\n%s" % self
.current_rep_table
[k
]
737 text
= "%s" % self
.current_rep_table
[k
]
740 def dumpstr_needed_replica_table(self
):
741 '''Debug dump string output of needed replica table'''
743 for k
in self
.needed_rep_table
.keys():
745 text
= text
+ "\n%s" % self
.needed_rep_table
[k
]
747 text
= "%s" % self
.needed_rep_table
[k
]
750 def dumpstr_connect_table(self
):
751 '''Debug dump string output of connect table'''
753 for k
in self
.connect_table
.keys():
755 text
= text
+ "\n%s" % self
.connect_table
[k
]
757 text
= "%s" % self
.connect_table
[k
]
760 def new_connection(self
, options
, flags
, transport
, from_dnstr
, sched
):
761 """Set up a new connection for the DSA based on input
762 parameters. Connection will be added to the DSA
763 connect_table and will be marked as "to be added" pending
764 a call to commit_connections()
766 dnstr
= "CN=%s," % str(uuid
.uuid4()) + self
.dsa_dnstr
768 connect
= NTDSConnection(dnstr
)
769 connect
.to_be_added
= True
770 connect
.enabled
= True
771 connect
.from_dnstr
= from_dnstr
772 connect
.options
= options
773 connect
.flags
= flags
775 if transport
is not None:
776 connect
.transport_dnstr
= transport
.dnstr
778 if sched
is not None:
779 connect
.schedule
= sched
781 # Create schedule. Attribute valuse set according to MS-TECH
782 # intrasite connection creation document
783 connect
.schedule
= drsblobs
.schedule()
785 connect
.schedule
.size
= 188
786 connect
.schedule
.bandwidth
= 0
787 connect
.schedule
.numberOfSchedules
= 1
789 header
= drsblobs
.scheduleHeader()
793 connect
.schedule
.headerArray
= [ header
]
795 # 168 byte instances of the 0x01 value. The low order 4 bits
796 # of the byte equate to 15 minute intervals within a single hour.
797 # There are 168 bytes because there are 168 hours in a full week
798 # Effectively we are saying to perform replication at the end of
799 # each hour of the week
800 data
= drsblobs
.scheduleSlots()
801 data
.slots
= [ 0x01 ] * 168
803 connect
.schedule
.dataArray
= [ data
]
805 self
.add_connection(dnstr
, connect
);
809 class NTDSConnection(object):
810 """Class defines a nTDSConnection found under a DSA
812 def __init__(self
, dnstr
):
817 self
.to_be_added
= False # new connection needs to be added
818 self
.to_be_deleted
= False # old connection needs to be deleted
819 self
.to_be_modified
= False
821 self
.system_flags
= 0
822 self
.transport_dnstr
= None
823 self
.transport_guid
= None
824 self
.from_dnstr
= None
828 '''Debug dump string output of NTDSConnection object'''
830 text
= "%s:\n\tdn=%s" % (self
.__class
__.__name
__, self
.dnstr
)
831 text
= text
+ "\n\tenabled=%s" % self
.enabled
832 text
= text
+ "\n\tto_be_added=%s" % self
.to_be_added
833 text
= text
+ "\n\tto_be_deleted=%s" % self
.to_be_deleted
834 text
= text
+ "\n\tto_be_modified=%s" % self
.to_be_modified
835 text
= text
+ "\n\toptions=0x%08X" % self
.options
836 text
= text
+ "\n\tsystem_flags=0x%08X" % self
.system_flags
837 text
= text
+ "\n\twhenCreated=%d" % self
.whenCreated
838 text
= text
+ "\n\ttransport_dn=%s" % self
.transport_dnstr
840 if self
.guid
is not None:
841 text
= text
+ "\n\tguid=%s" % str(self
.guid
)
843 if self
.transport_guid
is not None:
844 text
= text
+ "\n\ttransport_guid=%s" % str(self
.transport_guid
)
846 text
= text
+ "\n\tfrom_dn=%s" % self
.from_dnstr
848 if self
.schedule
is not None:
849 text
= text
+ "\n\tschedule.size=%s" % self
.schedule
.size
850 text
= text
+ "\n\tschedule.bandwidth=%s" % self
.schedule
.bandwidth
851 text
= text
+ "\n\tschedule.numberOfSchedules=%s" % \
852 self
.schedule
.numberOfSchedules
854 for i
, header
in enumerate(self
.schedule
.headerArray
):
855 text
= text
+ "\n\tschedule.headerArray[%d].type=%d" % \
857 text
= text
+ "\n\tschedule.headerArray[%d].offset=%d" % \
859 text
= text
+ "\n\tschedule.dataArray[%d].slots[ " % i
860 for slot
in self
.schedule
.dataArray
[i
].slots
:
861 text
= text
+ "0x%X " % slot
866 def load_connection(self
, samdb
):
867 """Given a NTDSConnection object with an prior initialization
868 for the object's DN, search for the DN and load attributes
880 res
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
,
883 except ldb
.LdbError
, (enum
, estr
):
884 raise Exception("Unable to find nTDSConnection for (%s) - (%s)" %
890 self
.options
= int(msg
["options"][0])
892 if "enabledConnection" in msg
:
893 if msg
["enabledConnection"][0].upper().lstrip().rstrip() == "TRUE":
896 if "systemFlags" in msg
:
897 self
.system_flags
= int(msg
["systemFlags"][0])
899 if "objectGUID" in msg
:
901 misc
.GUID(samdb
.schema_format_value("objectGUID",
902 msg
["objectGUID"][0]))
904 if "transportType" in msg
:
905 dsdn
= dsdb_Dn(samdb
, msg
["tranportType"][0])
906 self
.load_connection_transport(str(dsdn
.dn
))
908 if "schedule" in msg
:
909 self
.schedule
= ndr_unpack(drsblobs
.replSchedule
, msg
["schedule"][0])
911 if "whenCreated" in msg
:
912 self
.whenCreated
= ldb
.string_to_time(msg
["whenCreated"][0])
914 if "fromServer" in msg
:
915 dsdn
= dsdb_Dn(samdb
, msg
["fromServer"][0])
916 self
.from_dnstr
= str(dsdn
.dn
)
917 assert self
.from_dnstr
is not None
919 def load_connection_transport(self
, tdnstr
):
920 """Given a NTDSConnection object which enumerates a transport
921 DN, load the transport information for the connection object
923 :param tdnstr: transport DN to load
925 attrs
= [ "objectGUID" ]
927 res
= samdb
.search(base
=tdnstr
,
928 scope
=ldb
.SCOPE_BASE
, attrs
=attrs
)
930 except ldb
.LdbError
, (enum
, estr
):
931 raise Exception("Unable to find transport (%s)" %
934 if "objectGUID" in res
[0]:
935 self
.transport_dnstr
= tdnstr
936 self
.transport_guid
= \
937 misc
.GUID(samdb
.schema_format_value("objectGUID",
938 msg
["objectGUID"][0]))
939 assert self
.transport_dnstr
is not None
940 assert self
.transport_guid
is not None
942 def commit_deleted(self
, samdb
, ro
=False):
943 """Local helper routine for commit_connections() which
944 handles committed connections that are to be deleted from
945 the database database
947 assert self
.to_be_deleted
948 self
.to_be_deleted
= False
950 # No database modification requested
955 samdb
.delete(self
.dnstr
)
956 except ldb
.LdbError
, (enum
, estr
):
957 raise Exception("Could not delete nTDSConnection for (%s) - (%s)" %
960 def commit_added(self
, samdb
, ro
=False):
961 """Local helper routine for commit_connections() which
962 handles committed connections that are to be added to the
965 assert self
.to_be_added
966 self
.to_be_added
= False
968 # No database modification requested
972 # First verify we don't have this entry to ensure nothing
973 # is programatically amiss
976 msg
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
)
980 except ldb
.LdbError
, (enum
, estr
):
981 if enum
!= ldb
.ERR_NO_SUCH_OBJECT
:
982 raise Exception("Unable to search for (%s) - (%s)" %
985 raise Exception("nTDSConnection for (%s) already exists!" %
993 # Prepare a message for adding to the samdb
995 m
.dn
= ldb
.Dn(samdb
, self
.dnstr
)
998 ldb
.MessageElement("nTDSConnection", ldb
.FLAG_MOD_ADD
,
1000 m
["showInAdvancedViewOnly"] = \
1001 ldb
.MessageElement("TRUE", ldb
.FLAG_MOD_ADD
,
1002 "showInAdvancedViewOnly")
1003 m
["enabledConnection"] = \
1004 ldb
.MessageElement(enablestr
, ldb
.FLAG_MOD_ADD
, "enabledConnection")
1006 ldb
.MessageElement(self
.from_dnstr
, ldb
.FLAG_MOD_ADD
, "fromServer")
1008 ldb
.MessageElement(str(self
.options
), ldb
.FLAG_MOD_ADD
, "options")
1009 m
["systemFlags"] = \
1010 ldb
.MessageElement(str(self
.system_flags
), ldb
.FLAG_MOD_ADD
,
1013 if self
.transport_dnstr
is not None:
1014 m
["transportType"] = \
1015 ldb
.MessageElement(str(self
.transport_dnstr
), ldb
.FLAG_MOD_ADD
,
1018 if self
.schedule
is not None:
1020 ldb
.MessageElement(ndr_pack(self
.schedule
),
1021 ldb
.FLAG_MOD_ADD
, "schedule")
1024 except ldb
.LdbError
, (enum
, estr
):
1025 raise Exception("Could not add nTDSConnection for (%s) - (%s)" %
1028 def commit_modified(self
, samdb
, ro
=False):
1029 """Local helper routine for commit_connections() which
1030 handles committed connections that are to be modified to the
1033 assert self
.to_be_modified
1034 self
.to_be_modified
= False
1036 # No database modification requested
1040 # First verify we have this entry to ensure nothing
1041 # is programatically amiss
1043 msg
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
)
1046 except ldb
.LdbError
, (enum
, estr
):
1047 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
1050 raise Exception("Unable to search for (%s) - (%s)" %
1053 raise Exception("nTDSConnection for (%s) doesn't exist!" %
1061 # Prepare a message for modifying the samdb
1063 m
.dn
= ldb
.Dn(samdb
, self
.dnstr
)
1065 m
["enabledConnection"] = \
1066 ldb
.MessageElement(enablestr
, ldb
.FLAG_MOD_REPLACE
,
1067 "enabledConnection")
1069 ldb
.MessageElement(self
.from_dnstr
, ldb
.FLAG_MOD_REPLACE
,
1072 ldb
.MessageElement(str(self
.options
), ldb
.FLAG_MOD_REPLACE
,
1074 m
["systemFlags"] = \
1075 ldb
.MessageElement(str(self
.system_flags
), ldb
.FLAG_MOD_REPLACE
,
1078 if self
.transport_dnstr
is not None:
1079 m
["transportType"] = \
1080 ldb
.MessageElement(str(self
.transport_dnstr
),
1081 ldb
.FLAG_MOD_REPLACE
, "transportType")
1083 m
["transportType"] = \
1084 ldb
.MessageElement([], ldb
.FLAG_MOD_DELETE
, "transportType")
1086 if self
.schedule
is not None:
1088 ldb
.MessageElement(ndr_pack(self
.schedule
),
1089 ldb
.FLAG_MOD_REPLACE
, "schedule")
1092 ldb
.MessageElement([], ldb
.FLAG_MOD_DELETE
, "schedule")
1095 except ldb
.LdbError
, (enum
, estr
):
1096 raise Exception("Could not modify nTDSConnection for (%s) - (%s)" %
1099 def set_modified(self
, truefalse
):
1100 self
.to_be_modified
= truefalse
1102 def set_added(self
, truefalse
):
1103 self
.to_be_added
= truefalse
1105 def set_deleted(self
, truefalse
):
1106 self
.to_be_deleted
= truefalse
1108 def is_schedule_minimum_once_per_week(self
):
1109 """Returns True if our schedule includes at least one
1110 replication interval within the week. False otherwise
1112 if self
.schedule
is None or self
.schedule
.dataArray
[0] is None:
1115 for slot
in self
.schedule
.dataArray
[0].slots
:
1116 if (slot
& 0x0F) != 0x0:
1120 def is_equivalent_schedule(self
, sched
):
1121 """Returns True if our schedule is equivalent to the input
1122 comparison schedule.
1124 :param shed: schedule to compare to
1126 if self
.schedule
is not None:
1132 if (self
.schedule
.size
!= sched
.size
or
1133 self
.schedule
.bandwidth
!= sched
.bandwidth
or
1134 self
.schedule
.numberOfSchedules
!= sched
.numberOfSchedules
):
1137 for i
, header
in enumerate(self
.schedule
.headerArray
):
1139 if self
.schedule
.headerArray
[i
].type != sched
.headerArray
[i
].type:
1142 if self
.schedule
.headerArray
[i
].offset
!= \
1143 sched
.headerArray
[i
].offset
:
1146 for a
, b
in zip(self
.schedule
.dataArray
[i
].slots
,
1147 sched
.dataArray
[i
].slots
):
1152 def convert_schedule_to_repltimes(self
):
1153 """Convert NTDS Connection schedule to replTime schedule.
1155 NTDS Connection schedule slots are double the size of
1156 the replTime slots but the top portion of the NTDS
1157 Connection schedule slot (4 most significant bits in
1158 uchar) are unused. The 4 least significant bits have
1159 the same (15 minute interval) bit positions as replTimes.
1160 We thus pack two elements of the NTDS Connection schedule
1161 slots into one element of the replTimes slot
1162 If no schedule appears in NTDS Connection then a default
1163 of 0x11 is set in each replTimes slot as per behaviour
1164 noted in a Windows DC. That default would cause replication
1165 within the last 15 minutes of each hour.
1169 for i
, slot
in enumerate(times
):
1170 if self
.schedule
is not None and \
1171 self
.schedule
.dataArray
[0] is not None:
1172 slot
= (self
.schedule
.dataArray
[0].slots
[i
*2] & 0xF) << 4 | \
1173 (self
.schedule
.dataArray
[0].slots
[i
*2] & 0xF)
1176 def is_rodc_topology(self
):
1177 """Returns True if NTDS Connection specifies RODC
1180 if self
.options
& dsdb
.NTDSCONN_OPT_RODC_TOPOLOGY
== 0:
1184 def is_generated(self
):
1185 """Returns True if NTDS Connection was generated by the
1186 KCC topology algorithm as opposed to set by the administrator
1188 if self
.options
& dsdb
.NTDSCONN_OPT_IS_GENERATED
== 0:
1192 def is_override_notify_default(self
):
1193 """Returns True if NTDS Connection should override notify default
1195 if self
.options
& dsdb
.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT
== 0:
1199 def is_use_notify(self
):
1200 """Returns True if NTDS Connection should use notify
1202 if self
.options
& dsdb
.NTDSCONN_OPT_USE_NOTIFY
== 0:
1206 def is_twoway_sync(self
):
1207 """Returns True if NTDS Connection should use twoway sync
1209 if self
.options
& dsdb
.NTDSCONN_OPT_TWOWAY_SYNC
== 0:
1213 def is_intersite_compression_disabled(self
):
1214 """Returns True if NTDS Connection intersite compression
1217 if self
.options
& dsdb
.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
== 0:
1221 def is_user_owned_schedule(self
):
1222 """Returns True if NTDS Connection has a user owned schedule
1224 if self
.options
& dsdb
.NTDSCONN_OPT_USER_OWNED_SCHEDULE
== 0:
1228 def is_enabled(self
):
1229 """Returns True if NTDS Connection is enabled
1233 def get_from_dnstr(self
):
1234 '''Return fromServer dn string attribute'''
1235 return self
.from_dnstr
1238 class Partition(NamingContext
):
1239 """A naming context discovered thru Partitions DN of the config schema.
1241 This is a more specific form of NamingContext class (inheriting from that
1242 class) and it identifies unique attributes enumerated in the Partitions
1243 such as which nTDSDSAs are cross referenced for replicas
1245 def __init__(self
, partstr
):
1246 self
.partstr
= partstr
1248 self
.system_flags
= 0
1249 self
.rw_location_list
= []
1250 self
.ro_location_list
= []
1252 # We don't have enough info to properly
1253 # fill in the naming context yet. We'll get that
1254 # fully set up with load_partition().
1255 NamingContext
.__init
__(self
, None)
1258 def load_partition(self
, samdb
):
1259 """Given a Partition class object that has been initialized with its
1260 partition dn string, load the partition from the sam database, identify
1261 the type of the partition (schema, domain, etc) and record the list of
1262 nTDSDSAs that appear in the cross reference attributes
1263 msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations.
1265 :param samdb: sam database to load partition from
1270 "msDS-NC-Replica-Locations",
1271 "msDS-NC-RO-Replica-Locations" ]
1273 res
= samdb
.search(base
=self
.partstr
, scope
=ldb
.SCOPE_BASE
,
1276 except ldb
.LdbError
, (enum
, estr
):
1277 raise Exception("Unable to find partition for (%s) - (%s)" % (
1278 self
.partstr
, estr
))
1281 for k
in msg
.keys():
1286 if msg
[k
][0].upper().lstrip().rstrip() == "TRUE":
1289 self
.enabled
= False
1292 if k
== "systemFlags":
1293 self
.system_flags
= int(msg
[k
][0])
1296 for value
in msg
[k
]:
1297 dsdn
= dsdb_Dn(samdb
, value
)
1298 dnstr
= str(dsdn
.dn
)
1301 self
.nc_dnstr
= dnstr
1304 if k
== "msDS-NC-Replica-Locations":
1305 self
.rw_location_list
.append(dnstr
)
1308 if k
== "msDS-NC-RO-Replica-Locations":
1309 self
.ro_location_list
.append(dnstr
)
1312 # Now identify what type of NC this partition
1314 self
.identify_by_basedn(samdb
)
1316 def is_enabled(self
):
1317 """Returns True if partition is enabled
1319 return self
.is_enabled
1321 def is_foreign(self
):
1322 """Returns True if this is not an Active Directory NC in our
1323 forest but is instead something else (e.g. a foreign NC)
1325 if (self
.system_flags
& dsdb
.SYSTEM_FLAG_CR_NTDS_NC
) == 0:
1330 def should_be_present(self
, target_dsa
):
1331 """Tests whether this partition should have an NC replica
1332 on the target dsa. This method returns a tuple of
1333 needed=True/False, ro=True/False, partial=True/False
1335 :param target_dsa: should NC be present on target dsa
1341 # If this is the config, schema, or default
1342 # domain NC for the target dsa then it should
1344 if self
.nc_type
== NCType
.config
or \
1345 self
.nc_type
== NCType
.schema
or \
1346 (self
.nc_type
== NCType
.domain
and
1347 self
.nc_dnstr
== target_dsa
.default_dnstr
):
1350 # A writable replica of an application NC should be present
1351 # if there a cross reference to the target DSA exists. Depending
1352 # on whether the DSA is ro we examine which type of cross reference
1353 # to look for (msDS-NC-Replica-Locations or
1354 # msDS-NC-RO-Replica-Locations
1355 if self
.nc_type
== NCType
.application
:
1356 if target_dsa
.is_ro():
1357 if target_dsa
.dsa_dnstr
in self
.ro_location_list
:
1360 if target_dsa
.dsa_dnstr
in self
.rw_location_list
:
1363 # If the target dsa is a gc then a partial replica of a
1364 # domain NC (other than the DSAs default domain) should exist
1365 # if there is also a cross reference for the DSA
1366 if target_dsa
.is_gc() and \
1367 self
.nc_type
== NCType
.domain
and \
1368 self
.nc_dnstr
!= target_dsa
.default_dnstr
and \
1369 (target_dsa
.dsa_dnstr
in self
.ro_location_list
or
1370 target_dsa
.dsa_dnstr
in self
.rw_location_list
):
1374 # partial NCs are always readonly
1375 if needed
and (target_dsa
.is_ro() or partial
):
1378 return needed
, ro
, partial
1381 '''Debug dump string output of class'''
1382 text
= "%s" % NamingContext
.__str
__(self
)
1383 text
= text
+ "\n\tpartdn=%s" % self
.partstr
1384 for k
in self
.rw_location_list
:
1385 text
= text
+ "\n\tmsDS-NC-Replica-Locations=%s" % k
1386 for k
in self
.ro_location_list
:
1387 text
= text
+ "\n\tmsDS-NC-RO-Replica-Locations=%s" % k
1392 """An individual site object discovered thru the configuration
1393 naming context. Contains all DSAs that exist within the site
1395 def __init__(self
, site_dnstr
):
1396 self
.site_dnstr
= site_dnstr
1397 self
.site_options
= 0
1398 self
.site_topo_generator
= None
1399 self
.site_topo_failover
= 0 # appears to be in minutes
1402 def load_site(self
, samdb
):
1403 """Loads the NTDS Site Settions options attribute for the site
1404 as well as querying and loading all DSAs that appear within
1407 ssdn
= "CN=NTDS Site Settings,%s" % self
.site_dnstr
1409 "interSiteTopologyFailover",
1410 "interSiteTopologyGenerator"]
1412 res
= samdb
.search(base
=ssdn
, scope
=ldb
.SCOPE_BASE
,
1414 except ldb
.LdbError
, (enum
, estr
):
1415 raise Exception("Unable to find site settings for (%s) - (%s)" %
1419 if "options" in msg
:
1420 self
.site_options
= int(msg
["options"][0])
1422 if "interSiteTopologyGenerator" in msg
:
1423 self
.site_topo_generator
= str(msg
["interSiteTopologyGenerator"][0])
1425 if "interSiteTopologyFailover" in msg
:
1426 self
.site_topo_failover
= int(msg
["interSiteTopologyFailover"][0])
1428 self
.load_all_dsa(samdb
)
1430 def load_all_dsa(self
, samdb
):
1431 """Discover all nTDSDSA thru the sites entry and
1432 instantiate and load the DSAs. Each dsa is inserted
1433 into the dsa_table by dn string.
1436 res
= samdb
.search(self
.site_dnstr
,
1437 scope
=ldb
.SCOPE_SUBTREE
,
1438 expression
="(objectClass=nTDSDSA)")
1439 except ldb
.LdbError
, (enum
, estr
):
1440 raise Exception("Unable to find nTDSDSAs - (%s)" % estr
)
1446 if dnstr
in self
.dsa_table
.keys():
1449 dsa
= DirectoryServiceAgent(dnstr
)
1453 # Assign this dsa to my dsa table
1454 # and index by dsa dn
1455 self
.dsa_table
[dnstr
] = dsa
1457 def get_dsa_by_guidstr(self
, guidstr
):
1458 for dsa
in self
.dsa_table
.values():
1459 if str(dsa
.dsa_guid
) == guidstr
:
1463 def get_dsa(self
, dnstr
):
1464 """Return a previously loaded DSA object by consulting
1465 the sites dsa_table for the provided DSA dn string
1467 :return: None if DSA doesn't exist
1469 if dnstr
in self
.dsa_table
.keys():
1470 return self
.dsa_table
[dnstr
]
1473 def select_istg(self
, samdb
, mydsa
, ro
):
1474 """Determine if my DC should be an intersite topology
1475 generator. If my DC is the istg and is both a writeable
1476 DC and the database is opened in write mode then we perform
1477 an originating update to set the interSiteTopologyGenerator
1478 attribute in the NTDS Site Settings object. An RODC always
1479 acts as an ISTG for itself.
1481 # The KCC on an RODC always acts as an ISTG for itself
1483 mydsa
.dsa_is_istg
= True
1486 # Find configuration NC replica for my DSA
1487 for c_rep
in mydsa
.current_rep_table
.values():
1488 if c_rep
.is_config():
1492 raise Exception("Unable to find config NC replica for (%s)" %
1495 # Load repsFrom if not already loaded so we can get the current
1496 # state of the config replica and whether we are getting updates
1498 c_rep
.load_repsFrom(samdb
)
1500 # From MS-Tech ISTG selection:
1501 # First, the KCC on a writable DC determines whether it acts
1502 # as an ISTG for its site
1504 # Let s be the object such that s!lDAPDisplayName = nTDSDSA
1505 # and classSchema in s!objectClass.
1507 # Let D be the sequence of objects o in the site of the local
1508 # DC such that o!objectCategory = s. D is sorted in ascending
1509 # order by objectGUID.
1511 # Which is a fancy way of saying "sort all the nTDSDSA objects
1512 # in the site by guid in ascending order". Place sorted list
1517 unixnow
= int(time
.time()) # seconds since 1970
1518 ntnow
= unix2nttime(unixnow
) # double word number of 100 nanosecond
1519 # intervals since 1600s
1521 for dsa
in self
.dsa_table
.values():
1524 D_sort
.sort(sort_dsa_by_guid
)
1526 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1527 # if o!interSiteTopologyFailover is 0 or has no value.
1529 # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1530 # so it appears we have to turn f into the same interval
1532 # interSiteTopologyFailover (if set) appears to be in minutes
1533 # so we'll need to convert to senconds and then 100 nanosecond
1536 # 10,000,000 is number of 100 nanosecond intervals in a second
1537 if self
.site_topo_failover
== 0:
1538 f
= 2 * 60 * 60 * 10000000
1540 f
= self
.site_topo_failover
* 60 * 10000000
1542 # From MS-Tech ISTG selection:
1543 # If o != NULL and o!interSiteTopologyGenerator is not the
1544 # nTDSDSA object for the local DC and
1545 # o!interSiteTopologyGenerator is an element dj of sequence D:
1547 if self
.site_topo_generator
is not None and \
1548 self
.site_topo_generator
in self
.dsa_table
.keys():
1549 d_dsa
= self
.dsa_table
[self
.site_topo_generator
]
1550 j_idx
= D_sort
.index(d_dsa
)
1552 if d_dsa
is not None and d_dsa
is not mydsa
:
1553 # From MS-Tech ISTG selection:
1554 # Let c be the cursor in the replUpToDateVector variable
1555 # associated with the NC replica of the config NC such
1556 # that c.uuidDsa = dj!invocationId. If no such c exists
1557 # (No evidence of replication from current ITSG):
1561 # Else if the current time < c.timeLastSyncSuccess - f
1562 # (Evidence of time sync problem on current ISTG):
1566 # Else (Evidence of replication from current ITSG):
1568 # Let t = c.timeLastSyncSuccess.
1570 # last_success appears to be a double word containing
1571 # number of 100 nanosecond intervals since the 1600s
1572 if d_dsa
.dsa_ivid
!= c_rep
.source_dsa_invocation_id
:
1576 elif ntnow
< (c_rep
.last_success
- f
):
1581 t_time
= c_rep
.last_success
1583 # Otherwise (Nominate local DC as ISTG):
1584 # Let i be the integer such that di is the nTDSDSA
1585 # object for the local DC.
1586 # Let t = the current time.
1588 i_idx
= D_sort
.index(mydsa
)
1591 # Compute a function that maintains the current ISTG if
1592 # it is alive, cycles through other candidates if not.
1594 # Let k be the integer (i + ((current time - t) /
1595 # o!interSiteTopologyFailover)) MOD |D|.
1597 # Note: We don't want to divide by zero here so they must
1598 # have meant "f" instead of "o!interSiteTopologyFailover"
1599 k_idx
= (i_idx
+ ((ntnow
- t_time
) / f
)) % len(D_sort
)
1601 # The local writable DC acts as an ISTG for its site if and
1602 # only if dk is the nTDSDSA object for the local DC. If the
1603 # local DC does not act as an ISTG, the KCC skips the
1604 # remainder of this task.
1605 d_dsa
= D_sort
[k_idx
]
1606 d_dsa
.dsa_is_istg
= True
1608 # Update if we are the ISTG, otherwise return
1609 if d_dsa
is not mydsa
:
1613 if self
.site_topo_generator
== mydsa
.dsa_dnstr
:
1616 self
.site_topo_generator
= mydsa
.dsa_dnstr
1618 # If readonly database then do not perform a
1623 # Perform update to the samdb
1624 ssdn
= "CN=NTDS Site Settings,%s" % self
.site_dnstr
1627 m
.dn
= ldb
.Dn(samdb
, ssdn
)
1629 m
["interSiteTopologyGenerator"] = \
1630 ldb
.MessageElement(mydsa
.dsa_dnstr
, ldb
.FLAG_MOD_REPLACE
,
1631 "interSiteTopologyGenerator")
1635 except ldb
.LdbError
, estr
:
1637 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1641 def is_intrasite_topology_disabled(self
):
1642 '''Returns True if intra-site topology is disabled for site'''
1643 if (self
.site_options
&
1644 dsdb
.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED
) != 0:
1648 def is_intersite_topology_disabled(self
):
1649 '''Returns True if inter-site topology is disabled for site'''
1650 if (self
.site_options
&
1651 dsdb
.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED
) != 0:
1655 def is_random_bridgehead_disabled(self
):
1656 '''Returns True if selection of random bridgehead is disabled'''
1657 if (self
.site_options
&
1658 dsdb
.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED
) != 0:
1662 def is_detect_stale_disabled(self
):
1663 '''Returns True if detect stale is disabled for site'''
1664 if (self
.site_options
&
1665 dsdb
.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED
) != 0:
1669 def is_cleanup_ntdsconn_disabled(self
):
1670 '''Returns True if NTDS Connection cleanup is disabled for site'''
1671 if (self
.site_options
&
1672 dsdb
.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED
) != 0:
1676 def same_site(self
, dsa
):
1677 '''Return True if dsa is in this site'''
1678 if self
.get_dsa(dsa
.dsa_dnstr
):
1683 '''Debug dump string output of class'''
1684 text
= "%s:" % self
.__class
__.__name
__
1685 text
= text
+ "\n\tdn=%s" % self
.site_dnstr
1686 text
= text
+ "\n\toptions=0x%X" % self
.site_options
1687 text
= text
+ "\n\ttopo_generator=%s" % self
.site_topo_generator
1688 text
= text
+ "\n\ttopo_failover=%d" % self
.site_topo_failover
1689 for key
, dsa
in self
.dsa_table
.items():
1690 text
= text
+ "\n%s" % dsa
1694 class GraphNode(object):
1695 """A graph node describing a set of edges that should be directed to it.
1697 Each edge is a connection for a particular naming context replica directed
1698 from another node in the forest to this node.
1701 def __init__(self
, dsa_dnstr
, max_node_edges
):
1702 """Instantiate the graph node according to a DSA dn string
1704 :param max_node_edges: maximum number of edges that should ever
1705 be directed to the node
1707 self
.max_edges
= max_node_edges
1708 self
.dsa_dnstr
= dsa_dnstr
1712 text
= "%s:" % self
.__class
__.__name
__
1713 text
= text
+ "\n\tdsa_dnstr=%s" % self
.dsa_dnstr
1714 text
= text
+ "\n\tmax_edges=%d" % self
.max_edges
1716 for i
, edge
in enumerate(self
.edge_from
):
1717 text
= text
+ "\n\tedge_from[%d]=%s" % (i
, edge
)
1720 def add_edge_from(self
, from_dsa_dnstr
):
1721 """Add an edge from the dsa to our graph nodes edge from list
1723 :param from_dsa_dnstr: the dsa that the edge emanates from
1725 assert from_dsa_dnstr
is not None
1727 # No edges from myself to myself
1728 if from_dsa_dnstr
== self
.dsa_dnstr
:
1730 # Only one edge from a particular node
1731 if from_dsa_dnstr
in self
.edge_from
:
1733 # Not too many edges
1734 if len(self
.edge_from
) >= self
.max_edges
:
1736 self
.edge_from
.append(from_dsa_dnstr
)
1739 def add_edges_from_connections(self
, dsa
):
1740 """For each nTDSConnection object associated with a particular
1741 DSA, we test if it implies an edge to this graph node (i.e.
1742 the "fromServer" attribute). If it does then we add an
1743 edge from the server unless we are over the max edges for this
1746 :param dsa: dsa with a dnstr equivalent to his graph node
1748 for dnstr
, connect
in dsa
.connect_table
.items():
1749 self
.add_edge_from(connect
.from_dnstr
)
1751 def add_connections_from_edges(self
, dsa
):
1752 """For each edge directed to this graph node, ensure there
1753 is a corresponding nTDSConnection object in the dsa.
1755 for edge_dnstr
in self
.edge_from
:
1756 connect
= dsa
.get_connection_by_from_dnstr(edge_dnstr
)
1758 # For each edge directed to the NC replica that
1759 # "should be present" on the local DC, the KCC determines
1760 # whether an object c exists such that:
1762 # c is a child of the DC's nTDSDSA object.
1763 # c.objectCategory = nTDSConnection
1765 # Given the NC replica ri from which the edge is directed,
1766 # c.fromServer is the dsname of the nTDSDSA object of
1767 # the DC on which ri "is present".
1769 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1770 if connect
and not connect
.is_rodc_topology():
1775 # if no such object exists then the KCC adds an object
1776 # c with the following attributes
1780 # Generate a new dnstr for this nTDSConnection
1781 opt
= dsdb
.NTDSCONN_OPT_IS_GENERATED
1782 flags
= dsdb
.SYSTEM_FLAG_CONFIG_ALLOW_RENAME
+ \
1783 dsdb
.SYSTEM_FLAG_CONFIG_ALLOW_MOVE
1785 dsa
.create_connection(opt
, flags
, None, edge_dnstr
, None)
1787 def has_sufficient_edges(self
):
1788 '''Return True if we have met the maximum "from edges" criteria'''
1789 if len(self
.edge_from
) >= self
.max_edges
:
1794 class Transport(object):
1795 """Class defines a Inter-site transport found under Sites
1798 def __init__(self
, dnstr
):
1803 self
.address_attr
= None
1804 self
.bridgehead_list
= []
1807 '''Debug dump string output of Transport object'''
1809 text
= "%s:\n\tdn=%s" % (self
.__class
__.__name
__, self
.dnstr
)
1810 text
= text
+ "\n\tguid=%s" % str(self
.guid
)
1811 text
= text
+ "\n\toptions=%d" % self
.options
1812 text
= text
+ "\n\taddress_attr=%s" % self
.address_attr
1813 text
= text
+ "\n\tname=%s" % self
.name
1814 for dnstr
in self
.bridgehead_list
:
1815 text
= text
+ "\n\tbridgehead_list=%s" % dnstr
1819 def load_transport(self
, samdb
):
1820 """Given a Transport object with an prior initialization
1821 for the object's DN, search for the DN and load attributes
1824 attrs
= [ "objectGUID",
1827 "bridgeheadServerListBL",
1828 "transportAddressAttribute" ]
1830 res
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
,
1833 except ldb
.LdbError
, (enum
, estr
):
1834 raise Exception("Unable to find Transport for (%s) - (%s)" %
1838 self
.guid
= misc
.GUID(samdb
.schema_format_value("objectGUID",
1839 msg
["objectGUID"][0]))
1841 if "options" in msg
:
1842 self
.options
= int(msg
["options"][0])
1844 if "transportAddressAttribute" in msg
:
1845 self
.address_attr
= str(msg
["transportAddressAttribute"][0])
1848 self
.name
= str(msg
["name"][0])
1850 if "bridgeheadServerListBL" in msg
:
1851 for value
in msg
["bridgeheadServerListBL"]:
1852 dsdn
= dsdb_Dn(samdb
, value
)
1853 dnstr
= str(dsdn
.dn
)
1854 if dnstr
not in self
.bridgehead_list
:
1855 self
.bridgehead_list
.append(dnstr
)
1858 class RepsFromTo(object):
1859 """Class encapsulation of the NDR repsFromToBlob.
1861 Removes the necessity of external code having to
1862 understand about other_info or manipulation of
1865 def __init__(self
, nc_dnstr
=None, ndr_blob
=None):
1867 self
.__dict
__['to_be_deleted'] = False
1868 self
.__dict
__['nc_dnstr'] = nc_dnstr
1869 self
.__dict
__['update_flags'] = 0x0
1873 # There is a very subtle bug here with python
1874 # and our NDR code. If you assign directly to
1875 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1876 # then a proper python GC reference count is not
1879 # To work around this we maintain an internal
1880 # reference to "dns_name(x)" and "other_info" elements
1881 # of repsFromToBlob. This internal reference
1882 # is hidden within this class but it is why you
1883 # see statements like this below:
1885 # self.__dict__['ndr_blob'].ctr.other_info = \
1886 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1888 # That would appear to be a redundant assignment but
1889 # it is necessary to hold a proper python GC reference
1891 if ndr_blob
is None:
1892 self
.__dict
__['ndr_blob'] = drsblobs
.repsFromToBlob()
1893 self
.__dict
__['ndr_blob'].version
= 0x1
1894 self
.__dict
__['dns_name1'] = None
1895 self
.__dict
__['dns_name2'] = None
1897 self
.__dict
__['ndr_blob'].ctr
.other_info
= \
1898 self
.__dict
__['other_info'] = drsblobs
.repsFromTo1OtherInfo()
1901 self
.__dict
__['ndr_blob'] = ndr_blob
1902 self
.__dict
__['other_info'] = ndr_blob
.ctr
.other_info
1904 if ndr_blob
.version
== 0x1:
1905 self
.__dict
__['dns_name1'] = ndr_blob
.ctr
.other_info
.dns_name
1906 self
.__dict
__['dns_name2'] = None
1908 self
.__dict
__['dns_name1'] = ndr_blob
.ctr
.other_info
.dns_name1
1909 self
.__dict
__['dns_name2'] = ndr_blob
.ctr
.other_info
.dns_name2
1912 '''Debug dump string output of class'''
1914 text
= "%s:" % self
.__class
__.__name
__
1915 text
= text
+ "\n\tdnstr=%s" % self
.nc_dnstr
1916 text
= text
+ "\n\tupdate_flags=0x%X" % self
.update_flags
1918 text
= text
+ "\n\tversion=%d" % self
.version
1919 text
= text
+ "\n\tsource_dsa_obj_guid=%s" % \
1920 str(self
.source_dsa_obj_guid
)
1921 text
= text
+ "\n\tsource_dsa_invocation_id=%s" % \
1922 str(self
.source_dsa_invocation_id
)
1923 text
= text
+ "\n\ttransport_guid=%s" % \
1924 str(self
.transport_guid
)
1925 text
= text
+ "\n\treplica_flags=0x%X" % \
1927 text
= text
+ "\n\tconsecutive_sync_failures=%d" % \
1928 self
.consecutive_sync_failures
1929 text
= text
+ "\n\tlast_success=%s" % \
1931 text
= text
+ "\n\tlast_attempt=%s" % \
1933 text
= text
+ "\n\tdns_name1=%s" % \
1935 text
= text
+ "\n\tdns_name2=%s" % \
1937 text
= text
+ "\n\tschedule[ "
1938 for slot
in self
.schedule
:
1939 text
= text
+ "0x%X " % slot
1944 def __setattr__(self
, item
, value
):
1946 if item
in [ 'schedule', 'replica_flags', 'transport_guid',
1947 'source_dsa_obj_guid', 'source_dsa_invocation_id',
1948 'consecutive_sync_failures', 'last_success',
1951 if item
in ['replica_flags']:
1952 self
.__dict
__['update_flags'] |
= drsuapi
.DRSUAPI_DRS_UPDATE_FLAGS
1953 elif item
in ['schedule']:
1954 self
.__dict
__['update_flags'] |
= drsuapi
.DRSUAPI_DRS_UPDATE_SCHEDULE
1956 setattr(self
.__dict
__['ndr_blob'].ctr
, item
, value
)
1958 elif item
in ['dns_name1']:
1959 self
.__dict
__['dns_name1'] = value
1961 if self
.__dict
__['ndr_blob'].version
== 0x1:
1962 self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name
= \
1963 self
.__dict
__['dns_name1']
1965 self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name1
= \
1966 self
.__dict
__['dns_name1']
1968 elif item
in ['dns_name2']:
1969 self
.__dict
__['dns_name2'] = value
1971 if self
.__dict
__['ndr_blob'].version
== 0x1:
1972 raise AttributeError(item
)
1974 self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name2
= \
1975 self
.__dict
__['dns_name2']
1977 elif item
in ['nc_dnstr']:
1978 self
.__dict
__['nc_dnstr'] = value
1980 elif item
in ['to_be_deleted']:
1981 self
.__dict
__['to_be_deleted'] = value
1983 elif item
in ['version']:
1984 raise AttributeError, "Attempt to set readonly attribute %s" % item
1986 raise AttributeError, "Unknown attribute %s" % item
1988 self
.__dict
__['update_flags'] |
= drsuapi
.DRSUAPI_DRS_UPDATE_ADDRESS
1990 def __getattr__(self
, item
):
1991 """Overload of RepsFromTo attribute retrieval.
1993 Allows external code to ignore substructures within the blob
1995 if item
in [ 'schedule', 'replica_flags', 'transport_guid',
1996 'source_dsa_obj_guid', 'source_dsa_invocation_id',
1997 'consecutive_sync_failures', 'last_success',
1999 return getattr(self
.__dict
__['ndr_blob'].ctr
, item
)
2001 elif item
in ['version']:
2002 return self
.__dict
__['ndr_blob'].version
2004 elif item
in ['dns_name1']:
2005 if self
.__dict
__['ndr_blob'].version
== 0x1:
2006 return self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name
2008 return self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name1
2010 elif item
in ['dns_name2']:
2011 if self
.__dict
__['ndr_blob'].version
== 0x1:
2012 raise AttributeError(item
)
2014 return self
.__dict
__['ndr_blob'].ctr
.other_info
.dns_name2
2016 elif item
in ['to_be_deleted']:
2017 return self
.__dict
__['to_be_deleted']
2019 elif item
in ['nc_dnstr']:
2020 return self
.__dict
__['nc_dnstr']
2022 elif item
in ['update_flags']:
2023 return self
.__dict
__['update_flags']
2025 raise AttributeError, "Unknwown attribute %s" % item
2027 def is_modified(self
):
2028 return (self
.update_flags
!= 0x0)
2030 def set_unmodified(self
):
2031 self
.__dict
__['update_flags'] = 0x0
2034 class SiteLink(object):
2035 """Class defines a site link found under sites
2038 def __init__(self
, dnstr
):
2041 self
.system_flags
= 0
2043 self
.schedule
= None
2044 self
.interval
= None
2048 '''Debug dump string output of Transport object'''
2050 text
= "%s:\n\tdn=%s" % (self
.__class
__.__name
__, self
.dnstr
)
2051 text
= text
+ "\n\toptions=%d" % self
.options
2052 text
= text
+ "\n\tsystem_flags=%d" % self
.system_flags
2053 text
= text
+ "\n\tcost=%d" % self
.cost
2054 text
= text
+ "\n\tinterval=%s" % self
.interval
2056 if self
.schedule
is not None:
2057 text
= text
+ "\n\tschedule.size=%s" % self
.schedule
.size
2058 text
= text
+ "\n\tschedule.bandwidth=%s" % self
.schedule
.bandwidth
2059 text
= text
+ "\n\tschedule.numberOfSchedules=%s" % \
2060 self
.schedule
.numberOfSchedules
2062 for i
, header
in enumerate(self
.schedule
.headerArray
):
2063 text
= text
+ "\n\tschedule.headerArray[%d].type=%d" % \
2065 text
= text
+ "\n\tschedule.headerArray[%d].offset=%d" % \
2067 text
= text
+ "\n\tschedule.dataArray[%d].slots[ " % i
2068 for slot
in self
.schedule
.dataArray
[i
].slots
:
2069 text
= text
+ "0x%X " % slot
2072 for dnstr
in self
.site_list
:
2073 text
= text
+ "\n\tsite_list=%s" % dnstr
2076 def load_sitelink(self
, samdb
):
2077 """Given a siteLink object with an prior initialization
2078 for the object's DN, search for the DN and load attributes
2081 attrs
= [ "options",
2088 res
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
,
2091 except ldb
.LdbError
, (enum
, estr
):
2092 raise Exception("Unable to find SiteLink for (%s) - (%s)" %
2097 if "options" in msg
:
2098 self
.options
= int(msg
["options"][0])
2100 if "systemFlags" in msg
:
2101 self
.system_flags
= int(msg
["systemFlags"][0])
2104 self
.cost
= int(msg
["cost"][0])
2106 if "replInterval" in msg
:
2107 self
.interval
= int(msg
["replInterval"][0])
2109 if "siteList" in msg
:
2110 for value
in msg
["siteList"]:
2111 dsdn
= dsdb_Dn(samdb
, value
)
2112 dnstr
= str(dsdn
.dn
)
2113 if dnstr
not in self
.site_list
:
2114 self
.site_list
.append(dnstr
)
2116 def is_sitelink(self
, site1_dnstr
, site2_dnstr
):
2117 """Given a siteLink object, determine if it is a link
2118 between the two input site DNs
2120 if site1_dnstr
in self
.site_list
and site2_dnstr
in self
.site_list
:
2125 class VertexColor(object):
2126 (unknown
, white
, black
, red
) = range(0, 4)
2129 class Vertex(object):
2130 """Class encapsulation of a Site Vertex in the
2131 intersite topology replication algorithm
2133 def __init__(self
, site
, part
):
2136 self
.color
= VertexColor
.unknown
2138 def color_vertex(self
):
2139 """Color each vertex to indicate which kind of NC
2142 # IF s contains one or more DCs with full replicas of the
2144 # SET v.Color to COLOR.RED
2145 # ELSEIF s contains one or more partial replicas of the NC
2146 # SET v.Color to COLOR.BLACK
2148 # SET v.Color to COLOR.WHITE
2150 # set to minimum (no replica)
2151 self
.color
= VertexColor
.white
2153 for dnstr
, dsa
in self
.site
.dsa_table
.items():
2154 rep
= dsa
.get_current_replica(self
.part
.nc_dnstr
)
2158 # We have a full replica which is the largest
2160 if not rep
.is_partial():
2161 self
.color
= VertexColor
.red
2164 self
.color
= VertexColor
.black
2167 assert(self
.color
!= VertexColor
.unknown
)
2168 return (self
.color
== VertexColor
.red
)
2171 assert(self
.color
!= VertexColor
.unknown
)
2172 return (self
.color
== VertexColor
.black
)
2175 assert(self
.color
!= VertexColor
.unknown
)
2176 return (self
.color
== VertexColor
.white
)
2178 ##################################################
2180 ##################################################
2181 def sort_dsa_by_guid(dsa1
, dsa2
):
2182 return cmp(dsa1
.dsa_guid
, dsa2
.dsa_guid
)