kcc: Reduce code verbosity in dumpstr_* functions
[Samba.git] / python / samba / kcc_utils.py
blob6d88a0d0a0807133b97d088d53866f691234678e
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/>.
19 import ldb
20 import uuid
21 import time
23 from samba import dsdb, unix2nttime
24 from samba.dcerpc import (
25 drsblobs,
26 drsuapi,
27 misc,
29 from samba.common import dsdb_Dn
30 from samba.ndr import (ndr_unpack, ndr_pack)
33 class NCType(object):
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
42 """
44 def __init__(self, nc_dnstr):
45 """Instantiate a NamingContext
47 :param nc_dnstr: NC dn string
48 """
49 self.nc_dnstr = nc_dnstr
50 self.nc_guid = None
51 self.nc_sid = None
52 self.nc_type = NCType.unknown
54 def __str__(self):
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>"
62 else:
63 text = text + "\n\tnc_sid=<present>"
65 text = text + "\n\tnc_type=%s" % self.nc_type
66 return text
68 def load_nc(self, samdb):
69 attrs = [ "objectGUID",
70 "objectSid" ]
71 try:
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) - (%s)" %
77 (self.nc_dnstr, estr))
78 msg = res[0]
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
87 def is_schema(self):
88 '''Return True if NC is schema'''
89 assert self.nc_type != NCType.unknown
90 return self.nc_type == NCType.schema
92 def is_domain(self):
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
102 def is_config(self):
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
113 # domain NCs)
114 if self.nc_guid is None:
115 self.load_nc(samdb)
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
127 else:
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
135 was found.
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
153 # identify those
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
180 self.rep_ro = False
181 self.rep_instantiated_flags = 0
183 self.rep_fsmo_role_owner = None
185 # RepsFromTo tuples
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)
198 def __str__(self):
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'''
216 if flags is None:
217 self.rep_instantiated_flags = 0
218 else:
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
236 # default domain NC
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
247 # basedn
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
255 # RODC only
256 elif attr == "msDS-hasFullReplicaNCs":
257 self.rep_present_criteria_one = True
258 self.rep_ro = True
260 # Not RODC
261 elif attr == "msDS-hasMasterNCs":
262 self.rep_ro = False
264 # Now use this DSA attribute to identify the naming
265 # context type by calling the super class method
266 # of the same name
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
274 def is_ro(self):
275 '''Return True if NC replica is read only'''
276 return self.rep_ro
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:
291 return True
292 return False
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
300 try:
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))
308 msg = res[0]
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
329 # older KCC also did
330 modify = False
331 newreps = []
332 delreps = []
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)
342 modify = True
343 continue
345 if repsFrom.is_modified():
346 repsFrom.set_unmodified()
347 modify = True
349 # current (unmodified) elements also get
350 # appended here but no changes will occur
351 # unless something is "to be modified" or
352 # "to be deleted"
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)
358 delreps = []
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
363 # record "as is"
364 if not modify or ro:
365 return
367 m = ldb.Message()
368 m.dn = ldb.Dn(samdb, self.nc_dnstr)
370 m["repsFrom"] = \
371 ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsFrom")
373 try:
374 samdb.modify(m)
376 except ldb.LdbError, estr:
377 raise Exception("Could not set repsFrom for (%s) - (%s)" %
378 (self.nc_dnstr, estr))
380 def load_replUpToDateVector(self, samdb):
381 """Given an NC replica which has been discovered thru the nTDSDSA
382 database object, load the replUpToDateVector attribute for the local replica.
383 held by my dsa. The replUpToDateVector attribute is not replicated so this
384 attribute is relative only to the local DSA that the samdb exists on
386 try:
387 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
388 attrs=[ "replUpToDateVector" ])
390 except ldb.LdbError, (enum, estr):
391 raise Exception("Unable to find NC for (%s) - (%s)" %
392 (self.nc_dnstr, estr))
394 msg = res[0]
396 # Possibly no replUpToDateVector if this is a singleton DC
397 if "replUpToDateVector" in msg:
398 value = msg["replUpToDateVector"][0]
399 replUpToDateVectorBlob = ndr_unpack(drsblobs.replUpToDateVectorBlob, value)
400 if replUpToDateVectorBlob.version != 2:
401 # Samba only generates version 2, and this runs locally
402 raise AttributeError("Unexpected replUpToDateVector version %d"
403 % replUpToDateVectorBlob.version)
405 self.rep_replUpToDateVector_cursors = replUpToDateVectorBlob.ctr.cursors
406 else:
407 self.rep_replUpToDateVector_cursors = []
409 def dumpstr_to_be_deleted(self):
410 return '\n'.join(str(x) for x in self.rep_repsFrom if x.to_be_deleted)
412 def dumpstr_to_be_modified(self):
413 return '\n'.join(str(x) for x in self.rep_repsFrom if x.is_modified())
415 def load_fsmo_roles(self, samdb):
416 """Given an NC replica which has been discovered thru the nTDSDSA
417 database object, load the fSMORoleOwner attribute.
419 try:
420 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
421 attrs=[ "fSMORoleOwner" ])
423 except ldb.LdbError, (enum, estr):
424 raise Exception("Unable to find NC for (%s) - (%s)" %
425 (self.nc_dnstr, estr))
427 msg = res[0]
429 # Possibly no fSMORoleOwner
430 if "fSMORoleOwner" in msg:
431 self.rep_fsmo_role_owner = msg["fSMORoleOwner"]
433 def is_fsmo_role_owner(self, dsa_dnstr):
434 if self.rep_fsmo_role_owner is not None and \
435 self.rep_fsmo_role_owner == dsa_dnstr:
436 return True
437 return False
440 class DirectoryServiceAgent(object):
442 def __init__(self, dsa_dnstr):
443 """Initialize DSA class.
445 Class is subsequently fully populated by calling the load_dsa() method
447 :param dsa_dnstr: DN of the nTDSDSA
449 self.dsa_dnstr = dsa_dnstr
450 self.dsa_guid = None
451 self.dsa_ivid = None
452 self.dsa_is_ro = False
453 self.dsa_is_istg = False
454 self.dsa_options = 0
455 self.dsa_behavior = 0
456 self.default_dnstr = None # default domain dn string for dsa
458 # NCReplicas for this dsa that are "present"
459 # Indexed by DN string of naming context
460 self.current_rep_table = {}
462 # NCReplicas for this dsa that "should be present"
463 # Indexed by DN string of naming context
464 self.needed_rep_table = {}
466 # NTDSConnections for this dsa. These are current
467 # valid connections that are committed or pending a commit
468 # in the database. Indexed by DN string of connection
469 self.connect_table = {}
471 def __str__(self):
472 '''Debug dump string output of class'''
474 text = "%s:" % self.__class__.__name__
475 if self.dsa_dnstr is not None:
476 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
477 if self.dsa_guid is not None:
478 text = text + "\n\tdsa_guid=%s" % str(self.dsa_guid)
479 if self.dsa_ivid is not None:
480 text = text + "\n\tdsa_ivid=%s" % str(self.dsa_ivid)
482 text = text + "\n\tro=%s" % self.is_ro()
483 text = text + "\n\tgc=%s" % self.is_gc()
484 text = text + "\n\tistg=%s" % self.is_istg()
486 text = text + "\ncurrent_replica_table:"
487 text = text + "\n%s" % self.dumpstr_current_replica_table()
488 text = text + "\nneeded_replica_table:"
489 text = text + "\n%s" % self.dumpstr_needed_replica_table()
490 text = text + "\nconnect_table:"
491 text = text + "\n%s" % self.dumpstr_connect_table()
493 return text
495 def get_current_replica(self, nc_dnstr):
496 if nc_dnstr in self.current_rep_table.keys():
497 return self.current_rep_table[nc_dnstr]
498 else:
499 return None
501 def is_istg(self):
502 '''Returns True if dsa is intersite topology generator for it's site'''
503 # The KCC on an RODC always acts as an ISTG for itself
504 return self.dsa_is_istg or self.dsa_is_ro
506 def is_ro(self):
507 '''Returns True if dsa a read only domain controller'''
508 return self.dsa_is_ro
510 def is_gc(self):
511 '''Returns True if dsa hosts a global catalog'''
512 if (self.options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
513 return True
514 return False
516 def is_minimum_behavior(self, version):
517 """Is dsa at minimum windows level greater than or equal to (version)
519 :param version: Windows version to test against
520 (e.g. DS_DOMAIN_FUNCTION_2008)
522 if self.dsa_behavior >= version:
523 return True
524 return False
526 def is_translate_ntdsconn_disabled(self):
527 """Whether this allows NTDSConnection translation in its options."""
528 if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0:
529 return True
530 return False
532 def get_rep_tables(self):
533 """Return DSA current and needed replica tables
535 return self.current_rep_table, self.needed_rep_table
537 def get_parent_dnstr(self):
538 """Get the parent DN string of this object."""
539 head, sep, tail = self.dsa_dnstr.partition(',')
540 return tail
542 def load_dsa(self, samdb):
543 """Load a DSA from the samdb.
545 Prior initialization has given us the DN of the DSA that we are to
546 load. This method initializes all other attributes, including loading
547 the NC replica table for this DSA.
549 attrs = ["objectGUID",
550 "invocationID",
551 "options",
552 "msDS-isRODC",
553 "msDS-Behavior-Version"]
554 try:
555 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
556 attrs=attrs)
558 except ldb.LdbError, (enum, estr):
559 raise Exception("Unable to find nTDSDSA for (%s) - (%s)" %
560 (self.dsa_dnstr, estr))
562 msg = res[0]
563 self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID",
564 msg["objectGUID"][0]))
566 # RODCs don't originate changes and thus have no invocationId,
567 # therefore we must check for existence first
568 if "invocationId" in msg:
569 self.dsa_ivid = misc.GUID(samdb.schema_format_value("objectGUID",
570 msg["invocationId"][0]))
572 if "options" in msg:
573 self.options = int(msg["options"][0])
575 if "msDS-isRODC" in msg and msg["msDS-isRODC"][0] == "TRUE":
576 self.dsa_is_ro = True
577 else:
578 self.dsa_is_ro = False
580 if "msDS-Behavior-Version" in msg:
581 self.dsa_behavior = int(msg['msDS-Behavior-Version'][0])
583 # Load the NC replicas that are enumerated on this dsa
584 self.load_current_replica_table(samdb)
586 # Load the nTDSConnection that are enumerated on this dsa
587 self.load_connection_table(samdb)
589 def load_current_replica_table(self, samdb):
590 """Method to load the NC replica's listed for DSA object.
592 This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs,
593 hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, and
594 msDS-HasInstantiatedNCs) to determine complete list of NC replicas that
595 are enumerated for the DSA. Once a NC replica is loaded it is
596 identified (schema, config, etc) and the other replica attributes
597 (partial, ro, etc) are determined.
599 :param samdb: database to query for DSA replica list
601 ncattrs = [ # not RODC - default, config, schema (old style)
602 "hasMasterNCs",
603 # not RODC - default, config, schema, app NCs
604 "msDS-hasMasterNCs",
605 # domain NC partial replicas
606 "hasPartialReplicaNCs",
607 # default domain NC
608 "msDS-HasDomainNCs",
609 # RODC only - default, config, schema, app NCs
610 "msDS-hasFullReplicaNCs",
611 # Identifies if replica is coming, going, or stable
612 "msDS-HasInstantiatedNCs" ]
613 try:
614 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
615 attrs=ncattrs)
617 except ldb.LdbError, (enum, estr):
618 raise Exception("Unable to find nTDSDSA NCs for (%s) - (%s)" %
619 (self.dsa_dnstr, estr))
621 # The table of NCs for the dsa we are searching
622 tmp_table = {}
624 # We should get one response to our query here for
625 # the ntds that we requested
626 if len(res[0]) > 0:
628 # Our response will contain a number of elements including
629 # the dn of the dsa as well as elements for each
630 # attribute (e.g. hasMasterNCs). Each of these elements
631 # is a dictonary list which we retrieve the keys for and
632 # then iterate over them
633 for k in res[0].keys():
634 if k == "dn":
635 continue
637 # For each attribute type there will be one or more DNs
638 # listed. For instance DCs normally have 3 hasMasterNCs
639 # listed.
640 for value in res[0][k]:
641 # Turn dn into a dsdb_Dn so we can use
642 # its methods to parse a binary DN
643 dsdn = dsdb_Dn(samdb, value)
644 flags = dsdn.get_binary_integer()
645 dnstr = str(dsdn.dn)
647 if not dnstr in tmp_table.keys():
648 rep = NCReplica(self.dsa_dnstr, self.dsa_guid, dnstr)
649 tmp_table[dnstr] = rep
650 else:
651 rep = tmp_table[dnstr]
653 if k == "msDS-HasInstantiatedNCs":
654 rep.set_instantiated_flags(flags)
655 continue
657 rep.identify_by_dsa_attr(samdb, k)
659 # if we've identified the default domain NC
660 # then save its DN string
661 if rep.is_default():
662 self.default_dnstr = dnstr
663 else:
664 raise Exception("No nTDSDSA NCs for (%s)" % self.dsa_dnstr)
666 # Assign our newly built NC replica table to this dsa
667 self.current_rep_table = tmp_table
669 def add_needed_replica(self, rep):
670 """Method to add a NC replica that "should be present" to the
671 needed_rep_table.
673 self.needed_rep_table[rep.nc_dnstr] = rep
675 def load_connection_table(self, samdb):
676 """Method to load the nTDSConnections listed for DSA object.
678 :param samdb: database to query for DSA connection list
680 try:
681 res = samdb.search(base=self.dsa_dnstr,
682 scope=ldb.SCOPE_SUBTREE,
683 expression="(objectClass=nTDSConnection)")
685 except ldb.LdbError, (enum, estr):
686 raise Exception("Unable to find nTDSConnection for (%s) - (%s)" %
687 (self.dsa_dnstr, estr))
689 for msg in res:
690 dnstr = str(msg.dn)
692 # already loaded
693 if dnstr in self.connect_table.keys():
694 continue
696 connect = NTDSConnection(dnstr)
698 connect.load_connection(samdb)
699 self.connect_table[dnstr] = connect
701 def commit_connections(self, samdb, ro=False):
702 """Method to commit any uncommitted nTDSConnections
703 modifications that are in our table. These would be
704 identified connections that are marked to be added or
705 deleted
707 :param samdb: database to commit DSA connection list to
708 :param ro: if (true) then peform internal operations but
709 do not write to the database (readonly)
711 delconn = []
713 for dnstr, connect in self.connect_table.items():
714 if connect.to_be_added:
715 connect.commit_added(samdb, ro)
717 if connect.to_be_modified:
718 connect.commit_modified(samdb, ro)
720 if connect.to_be_deleted:
721 connect.commit_deleted(samdb, ro)
722 delconn.append(dnstr)
724 # Now delete the connection from the table
725 for dnstr in delconn:
726 del self.connect_table[dnstr]
728 def add_connection(self, dnstr, connect):
729 assert dnstr not in self.connect_table.keys()
730 self.connect_table[dnstr] = connect
732 def get_connection_by_from_dnstr(self, from_dnstr):
733 """Scan DSA nTDSConnection table and return connection
734 with a "fromServer" dn string equivalent to method
735 input parameter.
737 :param from_dnstr: search for this from server entry
739 for dnstr, connect in self.connect_table.items():
740 if connect.get_from_dnstr() == from_dnstr:
741 return connect
742 return None
744 def dumpstr_current_replica_table(self):
745 '''Debug dump string output of current replica table'''
746 return '\n'.join(str(x) for x in self.current_rep_table)
748 def dumpstr_needed_replica_table(self):
749 '''Debug dump string output of needed replica table'''
750 return '\n'.join(str(x) for x in self.needed_rep_table)
752 def dumpstr_connect_table(self):
753 '''Debug dump string output of connect table'''
754 return '\n'.join(str(x) for x in self.connect_table)
756 def new_connection(self, options, flags, transport, from_dnstr, sched):
757 """Set up a new connection for the DSA based on input
758 parameters. Connection will be added to the DSA
759 connect_table and will be marked as "to be added" pending
760 a call to commit_connections()
762 dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
764 connect = NTDSConnection(dnstr)
765 connect.to_be_added = True
766 connect.enabled = True
767 connect.from_dnstr = from_dnstr
768 connect.options = options
769 connect.flags = flags
771 if transport is not None:
772 connect.transport_dnstr = transport.dnstr
773 connect.transport_guid = transport.guid
775 if sched is not None:
776 connect.schedule = sched
777 else:
778 # Create schedule. Attribute valuse set according to MS-TECH
779 # intrasite connection creation document
780 connect.schedule = drsblobs.schedule()
782 connect.schedule.size = 188
783 connect.schedule.bandwidth = 0
784 connect.schedule.numberOfSchedules = 1
786 header = drsblobs.scheduleHeader()
787 header.type = 0
788 header.offset = 20
790 connect.schedule.headerArray = [ header ]
792 # 168 byte instances of the 0x01 value. The low order 4 bits
793 # of the byte equate to 15 minute intervals within a single hour.
794 # There are 168 bytes because there are 168 hours in a full week
795 # Effectively we are saying to perform replication at the end of
796 # each hour of the week
797 data = drsblobs.scheduleSlots()
798 data.slots = [ 0x01 ] * 168
800 connect.schedule.dataArray = [ data ]
802 self.add_connection(dnstr, connect);
803 return connect
806 class NTDSConnection(object):
807 """Class defines a nTDSConnection found under a DSA
809 def __init__(self, dnstr):
810 self.dnstr = dnstr
811 self.guid = None
812 self.enabled = False
813 self.whenCreated = 0
814 self.to_be_added = False # new connection needs to be added
815 self.to_be_deleted = False # old connection needs to be deleted
816 self.to_be_modified = False
817 self.options = 0
818 self.system_flags = 0
819 self.transport_dnstr = None
820 self.transport_guid = None
821 self.from_dnstr = None
822 self.schedule = None
824 def __str__(self):
825 '''Debug dump string output of NTDSConnection object'''
827 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
828 text = text + "\n\tenabled=%s" % self.enabled
829 text = text + "\n\tto_be_added=%s" % self.to_be_added
830 text = text + "\n\tto_be_deleted=%s" % self.to_be_deleted
831 text = text + "\n\tto_be_modified=%s" % self.to_be_modified
832 text = text + "\n\toptions=0x%08X" % self.options
833 text = text + "\n\tsystem_flags=0x%08X" % self.system_flags
834 text = text + "\n\twhenCreated=%d" % self.whenCreated
835 text = text + "\n\ttransport_dn=%s" % self.transport_dnstr
837 if self.guid is not None:
838 text = text + "\n\tguid=%s" % str(self.guid)
840 if self.transport_guid is not None:
841 text = text + "\n\ttransport_guid=%s" % str(self.transport_guid)
843 text = text + "\n\tfrom_dn=%s" % self.from_dnstr
845 if self.schedule is not None:
846 text = text + "\n\tschedule.size=%s" % self.schedule.size
847 text = text + "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
848 text = text + "\n\tschedule.numberOfSchedules=%s" % \
849 self.schedule.numberOfSchedules
851 for i, header in enumerate(self.schedule.headerArray):
852 text = text + "\n\tschedule.headerArray[%d].type=%d" % \
853 (i, header.type)
854 text = text + "\n\tschedule.headerArray[%d].offset=%d" % \
855 (i, header.offset)
856 text = text + "\n\tschedule.dataArray[%d].slots[ " % i
857 for slot in self.schedule.dataArray[i].slots:
858 text = text + "0x%X " % slot
859 text = text + "]"
861 return text
863 def load_connection(self, samdb):
864 """Given a NTDSConnection object with an prior initialization
865 for the object's DN, search for the DN and load attributes
866 from the samdb.
868 attrs = [ "options",
869 "enabledConnection",
870 "schedule",
871 "whenCreated",
872 "objectGUID",
873 "transportType",
874 "fromServer",
875 "systemFlags" ]
876 try:
877 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
878 attrs=attrs)
880 except ldb.LdbError, (enum, estr):
881 raise Exception("Unable to find nTDSConnection for (%s) - (%s)" %
882 (self.dnstr, estr))
884 msg = res[0]
886 if "options" in msg:
887 self.options = int(msg["options"][0])
889 if "enabledConnection" in msg:
890 if msg["enabledConnection"][0].upper().lstrip().rstrip() == "TRUE":
891 self.enabled = True
893 if "systemFlags" in msg:
894 self.system_flags = int(msg["systemFlags"][0])
896 if "objectGUID" in msg:
897 self.guid = \
898 misc.GUID(samdb.schema_format_value("objectGUID",
899 msg["objectGUID"][0]))
901 if "transportType" in msg:
902 dsdn = dsdb_Dn(samdb, msg["transportType"][0])
903 self.load_connection_transport(samdb, str(dsdn.dn))
905 if "schedule" in msg:
906 self.schedule = ndr_unpack(drsblobs.schedule, msg["schedule"][0])
908 if "whenCreated" in msg:
909 self.whenCreated = ldb.string_to_time(msg["whenCreated"][0])
911 if "fromServer" in msg:
912 dsdn = dsdb_Dn(samdb, msg["fromServer"][0])
913 self.from_dnstr = str(dsdn.dn)
914 assert self.from_dnstr is not None
916 def load_connection_transport(self, samdb, tdnstr):
917 """Given a NTDSConnection object which enumerates a transport
918 DN, load the transport information for the connection object
920 :param tdnstr: transport DN to load
922 attrs = [ "objectGUID" ]
923 try:
924 res = samdb.search(base=tdnstr,
925 scope=ldb.SCOPE_BASE, attrs=attrs)
927 except ldb.LdbError, (enum, estr):
928 raise Exception("Unable to find transport (%s) - (%s)" %
929 (tdnstr, estr))
931 if "objectGUID" in res[0]:
932 msg = res[0]
933 self.transport_dnstr = tdnstr
934 self.transport_guid = \
935 misc.GUID(samdb.schema_format_value("objectGUID",
936 msg["objectGUID"][0]))
937 assert self.transport_dnstr is not None
938 assert self.transport_guid is not None
940 def commit_deleted(self, samdb, ro=False):
941 """Local helper routine for commit_connections() which
942 handles committed connections that are to be deleted from
943 the database database
945 assert self.to_be_deleted
946 self.to_be_deleted = False
948 # No database modification requested
949 if ro:
950 return
952 try:
953 samdb.delete(self.dnstr)
954 except ldb.LdbError, (enum, estr):
955 raise Exception("Could not delete nTDSConnection for (%s) - (%s)" %
956 (self.dnstr, estr))
958 def commit_added(self, samdb, ro=False):
959 """Local helper routine for commit_connections() which
960 handles committed connections that are to be added to the
961 database
963 assert self.to_be_added
964 self.to_be_added = False
966 # No database modification requested
967 if ro:
968 return
970 # First verify we don't have this entry to ensure nothing
971 # is programatically amiss
972 found = False
973 try:
974 msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
975 if len(msg) != 0:
976 found = True
978 except ldb.LdbError, (enum, estr):
979 if enum != ldb.ERR_NO_SUCH_OBJECT:
980 raise Exception("Unable to search for (%s) - (%s)" %
981 (self.dnstr, estr))
982 if found:
983 raise Exception("nTDSConnection for (%s) already exists!" %
984 self.dnstr)
986 if self.enabled:
987 enablestr = "TRUE"
988 else:
989 enablestr = "FALSE"
991 # Prepare a message for adding to the samdb
992 m = ldb.Message()
993 m.dn = ldb.Dn(samdb, self.dnstr)
995 m["objectClass"] = \
996 ldb.MessageElement("nTDSConnection", ldb.FLAG_MOD_ADD,
997 "objectClass")
998 m["showInAdvancedViewOnly"] = \
999 ldb.MessageElement("TRUE", ldb.FLAG_MOD_ADD,
1000 "showInAdvancedViewOnly")
1001 m["enabledConnection"] = \
1002 ldb.MessageElement(enablestr, ldb.FLAG_MOD_ADD, "enabledConnection")
1003 m["fromServer"] = \
1004 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_ADD, "fromServer")
1005 m["options"] = \
1006 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options")
1007 m["systemFlags"] = \
1008 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_ADD,
1009 "systemFlags")
1011 if self.transport_dnstr is not None:
1012 m["transportType"] = \
1013 ldb.MessageElement(str(self.transport_dnstr), ldb.FLAG_MOD_ADD,
1014 "transportType")
1016 if self.schedule is not None:
1017 m["schedule"] = \
1018 ldb.MessageElement(ndr_pack(self.schedule),
1019 ldb.FLAG_MOD_ADD, "schedule")
1020 try:
1021 samdb.add(m)
1022 except ldb.LdbError, (enum, estr):
1023 raise Exception("Could not add nTDSConnection for (%s) - (%s)" %
1024 (self.dnstr, estr))
1026 def commit_modified(self, samdb, ro=False):
1027 """Local helper routine for commit_connections() which
1028 handles committed connections that are to be modified to the
1029 database
1031 assert self.to_be_modified
1032 self.to_be_modified = False
1034 # No database modification requested
1035 if ro:
1036 return
1038 # First verify we have this entry to ensure nothing
1039 # is programatically amiss
1040 try:
1041 msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1042 found = True
1044 except ldb.LdbError, (enum, estr):
1045 if enum == ldb.ERR_NO_SUCH_OBJECT:
1046 found = False
1047 else:
1048 raise Exception("Unable to search for (%s) - (%s)" %
1049 (self.dnstr, estr))
1050 if not found:
1051 raise Exception("nTDSConnection for (%s) doesn't exist!" %
1052 self.dnstr)
1054 if self.enabled:
1055 enablestr = "TRUE"
1056 else:
1057 enablestr = "FALSE"
1059 # Prepare a message for modifying the samdb
1060 m = ldb.Message()
1061 m.dn = ldb.Dn(samdb, self.dnstr)
1063 m["enabledConnection"] = \
1064 ldb.MessageElement(enablestr, ldb.FLAG_MOD_REPLACE,
1065 "enabledConnection")
1066 m["fromServer"] = \
1067 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_REPLACE,
1068 "fromServer")
1069 m["options"] = \
1070 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_REPLACE,
1071 "options")
1072 m["systemFlags"] = \
1073 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_REPLACE,
1074 "systemFlags")
1076 if self.transport_dnstr is not None:
1077 m["transportType"] = \
1078 ldb.MessageElement(str(self.transport_dnstr),
1079 ldb.FLAG_MOD_REPLACE, "transportType")
1080 else:
1081 m["transportType"] = \
1082 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "transportType")
1084 if self.schedule is not None:
1085 m["schedule"] = \
1086 ldb.MessageElement(ndr_pack(self.schedule),
1087 ldb.FLAG_MOD_REPLACE, "schedule")
1088 else:
1089 m["schedule"] = \
1090 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "schedule")
1091 try:
1092 samdb.modify(m)
1093 except ldb.LdbError, (enum, estr):
1094 raise Exception("Could not modify nTDSConnection for (%s) - (%s)" %
1095 (self.dnstr, estr))
1097 def set_modified(self, truefalse):
1098 self.to_be_modified = truefalse
1100 def set_added(self, truefalse):
1101 self.to_be_added = truefalse
1103 def set_deleted(self, truefalse):
1104 self.to_be_deleted = truefalse
1106 def is_schedule_minimum_once_per_week(self):
1107 """Returns True if our schedule includes at least one
1108 replication interval within the week. False otherwise
1110 if self.schedule is None or self.schedule.dataArray[0] is None:
1111 return False
1113 for slot in self.schedule.dataArray[0].slots:
1114 if (slot & 0x0F) != 0x0:
1115 return True
1116 return False
1118 def is_equivalent_schedule(self, sched):
1119 """Returns True if our schedule is equivalent to the input
1120 comparison schedule.
1122 :param shed: schedule to compare to
1124 if self.schedule is not None:
1125 if sched is None:
1126 return False
1127 elif sched is None:
1128 return True
1130 if (self.schedule.size != sched.size or
1131 self.schedule.bandwidth != sched.bandwidth or
1132 self.schedule.numberOfSchedules != sched.numberOfSchedules):
1133 return False
1135 for i, header in enumerate(self.schedule.headerArray):
1137 if self.schedule.headerArray[i].type != sched.headerArray[i].type:
1138 return False
1140 if self.schedule.headerArray[i].offset != \
1141 sched.headerArray[i].offset:
1142 return False
1144 for a, b in zip(self.schedule.dataArray[i].slots,
1145 sched.dataArray[i].slots):
1146 if a != b:
1147 return False
1148 return True
1150 def convert_schedule_to_repltimes(self):
1151 """Convert NTDS Connection schedule to replTime schedule.
1153 NTDS Connection schedule slots are double the size of
1154 the replTime slots but the top portion of the NTDS
1155 Connection schedule slot (4 most significant bits in
1156 uchar) are unused. The 4 least significant bits have
1157 the same (15 minute interval) bit positions as replTimes.
1158 We thus pack two elements of the NTDS Connection schedule
1159 slots into one element of the replTimes slot
1160 If no schedule appears in NTDS Connection then a default
1161 of 0x11 is set in each replTimes slot as per behaviour
1162 noted in a Windows DC. That default would cause replication
1163 within the last 15 minutes of each hour.
1165 times = [0x11] * 84
1167 for i, slot in enumerate(times):
1168 if self.schedule is not None and \
1169 self.schedule.dataArray[0] is not None:
1170 slot = (self.schedule.dataArray[0].slots[i*2] & 0xF) << 4 | \
1171 (self.schedule.dataArray[0].slots[i*2] & 0xF)
1172 return times
1174 def is_rodc_topology(self):
1175 """Returns True if NTDS Connection specifies RODC
1176 topology only
1178 if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
1179 return False
1180 return True
1182 def is_generated(self):
1183 """Returns True if NTDS Connection was generated by the
1184 KCC topology algorithm as opposed to set by the administrator
1186 if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0:
1187 return False
1188 return True
1190 def is_override_notify_default(self):
1191 """Returns True if NTDS Connection should override notify default
1193 if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0:
1194 return False
1195 return True
1197 def is_use_notify(self):
1198 """Returns True if NTDS Connection should use notify
1200 if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0:
1201 return False
1202 return True
1204 def is_twoway_sync(self):
1205 """Returns True if NTDS Connection should use twoway sync
1207 if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0:
1208 return False
1209 return True
1211 def is_intersite_compression_disabled(self):
1212 """Returns True if NTDS Connection intersite compression
1213 is disabled
1215 if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0:
1216 return False
1217 return True
1219 def is_user_owned_schedule(self):
1220 """Returns True if NTDS Connection has a user owned schedule
1222 if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0:
1223 return False
1224 return True
1226 def is_enabled(self):
1227 """Returns True if NTDS Connection is enabled
1229 return self.enabled
1231 def get_from_dnstr(self):
1232 '''Return fromServer dn string attribute'''
1233 return self.from_dnstr
1236 class Partition(NamingContext):
1237 """A naming context discovered thru Partitions DN of the config schema.
1239 This is a more specific form of NamingContext class (inheriting from that
1240 class) and it identifies unique attributes enumerated in the Partitions
1241 such as which nTDSDSAs are cross referenced for replicas
1243 def __init__(self, partstr):
1244 self.partstr = partstr
1245 self.enabled = True
1246 self.system_flags = 0
1247 self.rw_location_list = []
1248 self.ro_location_list = []
1250 # We don't have enough info to properly
1251 # fill in the naming context yet. We'll get that
1252 # fully set up with load_partition().
1253 NamingContext.__init__(self, None)
1256 def load_partition(self, samdb):
1257 """Given a Partition class object that has been initialized with its
1258 partition dn string, load the partition from the sam database, identify
1259 the type of the partition (schema, domain, etc) and record the list of
1260 nTDSDSAs that appear in the cross reference attributes
1261 msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations.
1263 :param samdb: sam database to load partition from
1265 attrs = [ "nCName",
1266 "Enabled",
1267 "systemFlags",
1268 "msDS-NC-Replica-Locations",
1269 "msDS-NC-RO-Replica-Locations" ]
1270 try:
1271 res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE,
1272 attrs=attrs)
1274 except ldb.LdbError, (enum, estr):
1275 raise Exception("Unable to find partition for (%s) - (%s)" % (
1276 self.partstr, estr))
1278 msg = res[0]
1279 for k in msg.keys():
1280 if k == "dn":
1281 continue
1283 if k == "Enabled":
1284 if msg[k][0].upper().lstrip().rstrip() == "TRUE":
1285 self.enabled = True
1286 else:
1287 self.enabled = False
1288 continue
1290 if k == "systemFlags":
1291 self.system_flags = int(msg[k][0])
1292 continue
1294 for value in msg[k]:
1295 dsdn = dsdb_Dn(samdb, value)
1296 dnstr = str(dsdn.dn)
1298 if k == "nCName":
1299 self.nc_dnstr = dnstr
1300 continue
1302 if k == "msDS-NC-Replica-Locations":
1303 self.rw_location_list.append(dnstr)
1304 continue
1306 if k == "msDS-NC-RO-Replica-Locations":
1307 self.ro_location_list.append(dnstr)
1308 continue
1310 # Now identify what type of NC this partition
1311 # enumerated
1312 self.identify_by_basedn(samdb)
1314 def is_enabled(self):
1315 """Returns True if partition is enabled
1317 return self.is_enabled
1319 def is_foreign(self):
1320 """Returns True if this is not an Active Directory NC in our
1321 forest but is instead something else (e.g. a foreign NC)
1323 if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0:
1324 return True
1325 else:
1326 return False
1328 def should_be_present(self, target_dsa):
1329 """Tests whether this partition should have an NC replica
1330 on the target dsa. This method returns a tuple of
1331 needed=True/False, ro=True/False, partial=True/False
1333 :param target_dsa: should NC be present on target dsa
1335 needed = False
1336 ro = False
1337 partial = False
1339 # If this is the config, schema, or default
1340 # domain NC for the target dsa then it should
1341 # be present
1342 if self.nc_type == NCType.config or \
1343 self.nc_type == NCType.schema or \
1344 (self.nc_type == NCType.domain and
1345 self.nc_dnstr == target_dsa.default_dnstr):
1346 needed = True
1348 # A writable replica of an application NC should be present
1349 # if there a cross reference to the target DSA exists. Depending
1350 # on whether the DSA is ro we examine which type of cross reference
1351 # to look for (msDS-NC-Replica-Locations or
1352 # msDS-NC-RO-Replica-Locations
1353 if self.nc_type == NCType.application:
1354 if target_dsa.is_ro():
1355 if target_dsa.dsa_dnstr in self.ro_location_list:
1356 needed = True
1357 else:
1358 if target_dsa.dsa_dnstr in self.rw_location_list:
1359 needed = True
1361 # If the target dsa is a gc then a partial replica of a
1362 # domain NC (other than the DSAs default domain) should exist
1363 # if there is also a cross reference for the DSA
1364 if target_dsa.is_gc() and \
1365 self.nc_type == NCType.domain and \
1366 self.nc_dnstr != target_dsa.default_dnstr and \
1367 (target_dsa.dsa_dnstr in self.ro_location_list or
1368 target_dsa.dsa_dnstr in self.rw_location_list):
1369 needed = True
1370 partial = True
1372 # partial NCs are always readonly
1373 if needed and (target_dsa.is_ro() or partial):
1374 ro = True
1376 return needed, ro, partial
1378 def __str__(self):
1379 '''Debug dump string output of class'''
1380 text = "%s" % NamingContext.__str__(self)
1381 text = text + "\n\tpartdn=%s" % self.partstr
1382 for k in self.rw_location_list:
1383 text = text + "\n\tmsDS-NC-Replica-Locations=%s" % k
1384 for k in self.ro_location_list:
1385 text = text + "\n\tmsDS-NC-RO-Replica-Locations=%s" % k
1386 return text
1389 class Site(object):
1390 """An individual site object discovered thru the configuration
1391 naming context. Contains all DSAs that exist within the site
1393 def __init__(self, site_dnstr, unix_now):
1394 self.site_dnstr = site_dnstr
1395 self.site_guid = None
1396 self.site_options = 0
1397 self.site_topo_generator = None
1398 self.site_topo_failover = 0 # appears to be in minutes
1399 self.dsa_table = {}
1400 self.unix_now = unix_now
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
1405 the site.
1407 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1408 attrs = ["options",
1409 "interSiteTopologyFailover",
1410 "interSiteTopologyGenerator"]
1411 try:
1412 res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE,
1413 attrs=attrs)
1414 self_res = samdb.search(base=self.site_dnstr, scope=ldb.SCOPE_BASE,
1415 attrs=['objectGUID'])
1416 except ldb.LdbError, (enum, estr):
1417 raise Exception("Unable to find site settings for (%s) - (%s)" %
1418 (ssdn, estr))
1420 msg = res[0]
1421 if "options" in msg:
1422 self.site_options = int(msg["options"][0])
1424 if "interSiteTopologyGenerator" in msg:
1425 self.site_topo_generator = str(msg["interSiteTopologyGenerator"][0])
1427 if "interSiteTopologyFailover" in msg:
1428 self.site_topo_failover = int(msg["interSiteTopologyFailover"][0])
1430 msg = self_res[0]
1431 if "objectGUID" in msg:
1432 self.site_guid = misc.GUID(samdb.schema_format_value("objectGUID",
1433 msg["objectGUID"][0]))
1435 self.load_all_dsa(samdb)
1437 def load_all_dsa(self, samdb):
1438 """Discover all nTDSDSA thru the sites entry and
1439 instantiate and load the DSAs. Each dsa is inserted
1440 into the dsa_table by dn string.
1442 try:
1443 res = samdb.search(self.site_dnstr,
1444 scope=ldb.SCOPE_SUBTREE,
1445 expression="(objectClass=nTDSDSA)")
1446 except ldb.LdbError, (enum, estr):
1447 raise Exception("Unable to find nTDSDSAs - (%s)" % estr)
1449 for msg in res:
1450 dnstr = str(msg.dn)
1452 # already loaded
1453 if dnstr in self.dsa_table.keys():
1454 continue
1456 dsa = DirectoryServiceAgent(dnstr)
1458 dsa.load_dsa(samdb)
1460 # Assign this dsa to my dsa table
1461 # and index by dsa dn
1462 self.dsa_table[dnstr] = dsa
1464 def get_dsa_by_guidstr(self, guidstr):
1465 for dsa in self.dsa_table.values():
1466 if str(dsa.dsa_guid) == guidstr:
1467 return dsa
1468 return None
1470 def get_dsa(self, dnstr):
1471 """Return a previously loaded DSA object by consulting
1472 the sites dsa_table for the provided DSA dn string
1474 :return: None if DSA doesn't exist
1476 if dnstr in self.dsa_table.keys():
1477 return self.dsa_table[dnstr]
1478 return None
1480 def select_istg(self, samdb, mydsa, ro):
1481 """Determine if my DC should be an intersite topology
1482 generator. If my DC is the istg and is both a writeable
1483 DC and the database is opened in write mode then we perform
1484 an originating update to set the interSiteTopologyGenerator
1485 attribute in the NTDS Site Settings object. An RODC always
1486 acts as an ISTG for itself.
1488 # The KCC on an RODC always acts as an ISTG for itself
1489 if mydsa.dsa_is_ro:
1490 mydsa.dsa_is_istg = True
1491 return True
1493 # Find configuration NC replica for my DSA
1494 for c_rep in mydsa.current_rep_table.values():
1495 if c_rep.is_config():
1496 break
1498 if not c_rep.is_config():
1499 raise Exception("Unable to find config NC replica for (%s)" %
1500 mydsa.dsa_dnstr)
1502 # Load repsFrom and replUpToDateVector if not already loaded so we can get the current
1503 # state of the config replica and whether we are getting updates
1504 # from the istg
1505 c_rep.load_repsFrom(samdb)
1507 c_rep.load_replUpToDateVector(samdb)
1509 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1510 # First, the KCC on a writable DC determines whether it acts
1511 # as an ISTG for its site
1513 # Let s be the object such that s!lDAPDisplayName = nTDSDSA
1514 # and classSchema in s!objectClass.
1516 # Let D be the sequence of objects o in the site of the local
1517 # DC such that o!objectCategory = s. D is sorted in ascending
1518 # order by objectGUID.
1520 # Which is a fancy way of saying "sort all the nTDSDSA objects
1521 # in the site by guid in ascending order". Place sorted list
1522 # in D_sort[]
1523 D_sort = []
1524 d_dsa = None
1526 ntnow = unix2nttime(self.unix_now) # double word number of 100 nanosecond
1527 # intervals since 1600s
1529 for dsa in self.dsa_table.values():
1530 D_sort.append(dsa)
1532 D_sort.sort(sort_dsa_by_guid)
1534 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1535 # if o!interSiteTopologyFailover is 0 or has no value.
1537 # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1538 # so it appears we have to turn f into the same interval
1540 # interSiteTopologyFailover (if set) appears to be in minutes
1541 # so we'll need to convert to senconds and then 100 nanosecond
1542 # intervals
1544 # 10,000,000 is number of 100 nanosecond intervals in a second
1545 if self.site_topo_failover == 0:
1546 f = 2 * 60 * 60 * 10000000
1547 else:
1548 f = self.site_topo_failover * 60 * 10000000
1550 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1551 # If o != NULL and o!interSiteTopologyGenerator is not the
1552 # nTDSDSA object for the local DC and
1553 # o!interSiteTopologyGenerator is an element dj of sequence D:
1555 if self.site_topo_generator is not None and \
1556 self.site_topo_generator in self.dsa_table.keys():
1557 d_dsa = self.dsa_table[self.site_topo_generator]
1558 j_idx = D_sort.index(d_dsa)
1560 if d_dsa is not None and d_dsa is not mydsa:
1561 # From MS-ADTS 6.2.2.3.1 ISTG Selection:
1562 # Let c be the cursor in the replUpToDateVector variable
1563 # associated with the NC replica of the config NC such
1564 # that c.uuidDsa = dj!invocationId. If no such c exists
1565 # (No evidence of replication from current ITSG):
1566 # Let i = j.
1567 # Let t = 0.
1569 # Else if the current time < c.timeLastSyncSuccess - f
1570 # (Evidence of time sync problem on current ISTG):
1571 # Let i = 0.
1572 # Let t = 0.
1574 # Else (Evidence of replication from current ITSG):
1575 # Let i = j.
1576 # Let t = c.timeLastSyncSuccess.
1578 # last_success appears to be a double word containing
1579 # number of 100 nanosecond intervals since the 1600s
1580 found = False
1581 for cursor in c_rep.rep_replUpToDateVector_cursors:
1582 if d_dsa.dsa_ivid == cursor.source_dsa_invocation_id:
1583 found = True
1584 break
1586 if not found:
1587 i_idx = j_idx
1588 t_time = 0
1590 elif ntnow - cursor.last_sync_success > f:
1591 i_idx = 0
1592 t_time = 0
1593 else:
1594 i_idx = j_idx
1595 t_time = cursor.last_sync_success
1597 # Otherwise (Nominate local DC as ISTG):
1598 # Let i be the integer such that di is the nTDSDSA
1599 # object for the local DC.
1600 # Let t = the current time.
1601 else:
1602 i_idx = D_sort.index(mydsa)
1603 t_time = ntnow
1605 # Compute a function that maintains the current ISTG if
1606 # it is alive, cycles through other candidates if not.
1608 # Let k be the integer (i + ((current time - t) /
1609 # o!interSiteTopologyFailover)) MOD |D|.
1611 # Note: We don't want to divide by zero here so they must
1612 # have meant "f" instead of "o!interSiteTopologyFailover"
1613 k_idx = (i_idx + ((ntnow - t_time) / f)) % len(D_sort)
1615 # The local writable DC acts as an ISTG for its site if and
1616 # only if dk is the nTDSDSA object for the local DC. If the
1617 # local DC does not act as an ISTG, the KCC skips the
1618 # remainder of this task.
1619 d_dsa = D_sort[k_idx]
1620 d_dsa.dsa_is_istg = True
1622 # Update if we are the ISTG, otherwise return
1623 if d_dsa is not mydsa:
1624 return False
1626 # Nothing to do
1627 if self.site_topo_generator == mydsa.dsa_dnstr:
1628 return True
1630 self.site_topo_generator = mydsa.dsa_dnstr
1632 # If readonly database then do not perform a
1633 # persistent update
1634 if ro:
1635 return True
1637 # Perform update to the samdb
1638 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1640 m = ldb.Message()
1641 m.dn = ldb.Dn(samdb, ssdn)
1643 m["interSiteTopologyGenerator"] = \
1644 ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
1645 "interSiteTopologyGenerator")
1646 try:
1647 samdb.modify(m)
1649 except ldb.LdbError, estr:
1650 raise Exception(
1651 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1652 (ssdn, estr))
1653 return True
1655 def is_intrasite_topology_disabled(self):
1656 '''Returns True if intra-site topology is disabled for site'''
1657 if (self.site_options &
1658 dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0:
1659 return True
1660 return False
1662 def is_intersite_topology_disabled(self):
1663 '''Returns True if inter-site topology is disabled for site'''
1664 if (self.site_options &
1665 dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED) != 0:
1666 return True
1667 return False
1669 def is_random_bridgehead_disabled(self):
1670 '''Returns True if selection of random bridgehead is disabled'''
1671 if (self.site_options &
1672 dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0:
1673 return True
1674 return False
1676 def is_detect_stale_disabled(self):
1677 '''Returns True if detect stale is disabled for site'''
1678 if (self.site_options &
1679 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0:
1680 return True
1681 return False
1683 def is_cleanup_ntdsconn_disabled(self):
1684 '''Returns True if NTDS Connection cleanup is disabled for site'''
1685 if (self.site_options &
1686 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0:
1687 return True
1688 return False
1690 def same_site(self, dsa):
1691 '''Return True if dsa is in this site'''
1692 if self.get_dsa(dsa.dsa_dnstr):
1693 return True
1694 return False
1696 def __str__(self):
1697 '''Debug dump string output of class'''
1698 text = "%s:" % self.__class__.__name__
1699 text = text + "\n\tdn=%s" % self.site_dnstr
1700 text = text + "\n\toptions=0x%X" % self.site_options
1701 text = text + "\n\ttopo_generator=%s" % self.site_topo_generator
1702 text = text + "\n\ttopo_failover=%d" % self.site_topo_failover
1703 for key, dsa in self.dsa_table.items():
1704 text = text + "\n%s" % dsa
1705 return text
1708 class GraphNode(object):
1709 """A graph node describing a set of edges that should be directed to it.
1711 Each edge is a connection for a particular naming context replica directed
1712 from another node in the forest to this node.
1715 def __init__(self, dsa_dnstr, max_node_edges):
1716 """Instantiate the graph node according to a DSA dn string
1718 :param max_node_edges: maximum number of edges that should ever
1719 be directed to the node
1721 self.max_edges = max_node_edges
1722 self.dsa_dnstr = dsa_dnstr
1723 self.edge_from = []
1725 def __str__(self):
1726 text = "%s:" % self.__class__.__name__
1727 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
1728 text = text + "\n\tmax_edges=%d" % self.max_edges
1730 for i, edge in enumerate(self.edge_from):
1731 text = text + "\n\tedge_from[%d]=%s" % (i, edge)
1732 return text
1734 def add_edge_from(self, from_dsa_dnstr):
1735 """Add an edge from the dsa to our graph nodes edge from list
1737 :param from_dsa_dnstr: the dsa that the edge emanates from
1739 assert from_dsa_dnstr is not None
1741 # No edges from myself to myself
1742 if from_dsa_dnstr == self.dsa_dnstr:
1743 return False
1744 # Only one edge from a particular node
1745 if from_dsa_dnstr in self.edge_from:
1746 return False
1747 # Not too many edges
1748 if len(self.edge_from) >= self.max_edges:
1749 return False
1750 self.edge_from.append(from_dsa_dnstr)
1751 return True
1753 def add_edges_from_connections(self, dsa):
1754 """For each nTDSConnection object associated with a particular
1755 DSA, we test if it implies an edge to this graph node (i.e.
1756 the "fromServer" attribute). If it does then we add an
1757 edge from the server unless we are over the max edges for this
1758 graph node
1760 :param dsa: dsa with a dnstr equivalent to his graph node
1762 for dnstr, connect in dsa.connect_table.items():
1763 self.add_edge_from(connect.from_dnstr)
1765 def add_connections_from_edges(self, dsa):
1766 """For each edge directed to this graph node, ensure there
1767 is a corresponding nTDSConnection object in the dsa.
1769 for edge_dnstr in self.edge_from:
1770 connect = dsa.get_connection_by_from_dnstr(edge_dnstr)
1772 # For each edge directed to the NC replica that
1773 # "should be present" on the local DC, the KCC determines
1774 # whether an object c exists such that:
1776 # c is a child of the DC's nTDSDSA object.
1777 # c.objectCategory = nTDSConnection
1779 # Given the NC replica ri from which the edge is directed,
1780 # c.fromServer is the dsname of the nTDSDSA object of
1781 # the DC on which ri "is present".
1783 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1784 if connect and not connect.is_rodc_topology():
1785 exists = True
1786 else:
1787 exists = False
1789 # if no such object exists then the KCC adds an object
1790 # c with the following attributes
1791 if exists:
1792 return
1794 # Generate a new dnstr for this nTDSConnection
1795 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1796 flags = dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME + \
1797 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE
1799 dsa.new_connection(opt, flags, None, edge_dnstr, None)
1801 def has_sufficient_edges(self):
1802 '''Return True if we have met the maximum "from edges" criteria'''
1803 if len(self.edge_from) >= self.max_edges:
1804 return True
1805 return False
1808 class Transport(object):
1809 """Class defines a Inter-site transport found under Sites
1812 def __init__(self, dnstr):
1813 self.dnstr = dnstr
1814 self.options = 0
1815 self.guid = None
1816 self.name = None
1817 self.address_attr = None
1818 self.bridgehead_list = []
1820 def __str__(self):
1821 '''Debug dump string output of Transport object'''
1823 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
1824 text = text + "\n\tguid=%s" % str(self.guid)
1825 text = text + "\n\toptions=%d" % self.options
1826 text = text + "\n\taddress_attr=%s" % self.address_attr
1827 text = text + "\n\tname=%s" % self.name
1828 for dnstr in self.bridgehead_list:
1829 text = text + "\n\tbridgehead_list=%s" % dnstr
1831 return text
1833 def load_transport(self, samdb):
1834 """Given a Transport object with an prior initialization
1835 for the object's DN, search for the DN and load attributes
1836 from the samdb.
1838 attrs = [ "objectGUID",
1839 "options",
1840 "name",
1841 "bridgeheadServerListBL",
1842 "transportAddressAttribute" ]
1843 try:
1844 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
1845 attrs=attrs)
1847 except ldb.LdbError, (enum, estr):
1848 raise Exception("Unable to find Transport for (%s) - (%s)" %
1849 (self.dnstr, estr))
1851 msg = res[0]
1852 self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
1853 msg["objectGUID"][0]))
1855 if "options" in msg:
1856 self.options = int(msg["options"][0])
1858 if "transportAddressAttribute" in msg:
1859 self.address_attr = str(msg["transportAddressAttribute"][0])
1861 if "name" in msg:
1862 self.name = str(msg["name"][0])
1864 if "bridgeheadServerListBL" in msg:
1865 for value in msg["bridgeheadServerListBL"]:
1866 dsdn = dsdb_Dn(samdb, value)
1867 dnstr = str(dsdn.dn)
1868 if dnstr not in self.bridgehead_list:
1869 self.bridgehead_list.append(dnstr)
1872 class RepsFromTo(object):
1873 """Class encapsulation of the NDR repsFromToBlob.
1875 Removes the necessity of external code having to
1876 understand about other_info or manipulation of
1877 update flags.
1879 def __init__(self, nc_dnstr=None, ndr_blob=None):
1881 self.__dict__['to_be_deleted'] = False
1882 self.__dict__['nc_dnstr'] = nc_dnstr
1883 self.__dict__['update_flags'] = 0x0
1885 # WARNING:
1887 # There is a very subtle bug here with python
1888 # and our NDR code. If you assign directly to
1889 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1890 # then a proper python GC reference count is not
1891 # maintained.
1893 # To work around this we maintain an internal
1894 # reference to "dns_name(x)" and "other_info" elements
1895 # of repsFromToBlob. This internal reference
1896 # is hidden within this class but it is why you
1897 # see statements like this below:
1899 # self.__dict__['ndr_blob'].ctr.other_info = \
1900 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1902 # That would appear to be a redundant assignment but
1903 # it is necessary to hold a proper python GC reference
1904 # count.
1905 if ndr_blob is None:
1906 self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
1907 self.__dict__['ndr_blob'].version = 0x1
1908 self.__dict__['dns_name1'] = None
1909 self.__dict__['dns_name2'] = None
1911 self.__dict__['ndr_blob'].ctr.other_info = \
1912 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1914 else:
1915 self.__dict__['ndr_blob'] = ndr_blob
1916 self.__dict__['other_info'] = ndr_blob.ctr.other_info
1918 if ndr_blob.version == 0x1:
1919 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
1920 self.__dict__['dns_name2'] = None
1921 else:
1922 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
1923 self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
1925 def __str__(self):
1926 '''Debug dump string output of class'''
1928 text = "%s:" % self.__class__.__name__
1929 text = text + "\n\tdnstr=%s" % self.nc_dnstr
1930 text = text + "\n\tupdate_flags=0x%X" % self.update_flags
1932 text = text + "\n\tversion=%d" % self.version
1933 text = text + "\n\tsource_dsa_obj_guid=%s" % \
1934 str(self.source_dsa_obj_guid)
1935 text = text + "\n\tsource_dsa_invocation_id=%s" % \
1936 str(self.source_dsa_invocation_id)
1937 text = text + "\n\ttransport_guid=%s" % \
1938 str(self.transport_guid)
1939 text = text + "\n\treplica_flags=0x%X" % \
1940 self.replica_flags
1941 text = text + "\n\tconsecutive_sync_failures=%d" % \
1942 self.consecutive_sync_failures
1943 text = text + "\n\tlast_success=%s" % \
1944 self.last_success
1945 text = text + "\n\tlast_attempt=%s" % \
1946 self.last_attempt
1947 text = text + "\n\tdns_name1=%s" % \
1948 str(self.dns_name1)
1949 text = text + "\n\tdns_name2=%s" % \
1950 str(self.dns_name2)
1951 text = text + "\n\tschedule[ "
1952 for slot in self.schedule:
1953 text = text + "0x%X " % slot
1954 text = text + "]"
1956 return text
1958 def __setattr__(self, item, value):
1960 if item in [ 'schedule', 'replica_flags', 'transport_guid',
1961 'source_dsa_obj_guid', 'source_dsa_invocation_id',
1962 'consecutive_sync_failures', 'last_success',
1963 'last_attempt' ]:
1965 if item in ['replica_flags']:
1966 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
1967 elif item in ['schedule']:
1968 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
1970 setattr(self.__dict__['ndr_blob'].ctr, item, value)
1972 elif item in ['dns_name1']:
1973 self.__dict__['dns_name1'] = value
1975 if self.__dict__['ndr_blob'].version == 0x1:
1976 self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
1977 self.__dict__['dns_name1']
1978 else:
1979 self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
1980 self.__dict__['dns_name1']
1982 elif item in ['dns_name2']:
1983 self.__dict__['dns_name2'] = value
1985 if self.__dict__['ndr_blob'].version == 0x1:
1986 raise AttributeError(item)
1987 else:
1988 self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
1989 self.__dict__['dns_name2']
1991 elif item in ['nc_dnstr']:
1992 self.__dict__['nc_dnstr'] = value
1994 elif item in ['to_be_deleted']:
1995 self.__dict__['to_be_deleted'] = value
1997 elif item in ['version']:
1998 raise AttributeError("Attempt to set readonly attribute %s" % item)
1999 else:
2000 raise AttributeError("Unknown attribute %s" % item)
2002 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
2004 def __getattr__(self, item):
2005 """Overload of RepsFromTo attribute retrieval.
2007 Allows external code to ignore substructures within the blob
2009 if item in [ 'schedule', 'replica_flags', 'transport_guid',
2010 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2011 'consecutive_sync_failures', 'last_success',
2012 'last_attempt' ]:
2013 return getattr(self.__dict__['ndr_blob'].ctr, item)
2015 elif item in ['version']:
2016 return self.__dict__['ndr_blob'].version
2018 elif item in ['dns_name1']:
2019 if self.__dict__['ndr_blob'].version == 0x1:
2020 return self.__dict__['ndr_blob'].ctr.other_info.dns_name
2021 else:
2022 return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
2024 elif item in ['dns_name2']:
2025 if self.__dict__['ndr_blob'].version == 0x1:
2026 raise AttributeError(item)
2027 else:
2028 return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
2030 elif item in ['to_be_deleted']:
2031 return self.__dict__['to_be_deleted']
2033 elif item in ['nc_dnstr']:
2034 return self.__dict__['nc_dnstr']
2036 elif item in ['update_flags']:
2037 return self.__dict__['update_flags']
2039 raise AttributeError("Unknown attribute %s" % item)
2041 def is_modified(self):
2042 return (self.update_flags != 0x0)
2044 def set_unmodified(self):
2045 self.__dict__['update_flags'] = 0x0
2048 class SiteLink(object):
2049 """Class defines a site link found under sites
2052 def __init__(self, dnstr):
2053 self.dnstr = dnstr
2054 self.options = 0
2055 self.system_flags = 0
2056 self.cost = 0
2057 self.schedule = None
2058 self.interval = None
2059 self.site_list = []
2061 def __str__(self):
2062 '''Debug dump string output of Transport object'''
2064 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
2065 text = text + "\n\toptions=%d" % self.options
2066 text = text + "\n\tsystem_flags=%d" % self.system_flags
2067 text = text + "\n\tcost=%d" % self.cost
2068 text = text + "\n\tinterval=%s" % self.interval
2070 if self.schedule is not None:
2071 text = text + "\n\tschedule.size=%s" % self.schedule.size
2072 text = text + "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
2073 text = text + "\n\tschedule.numberOfSchedules=%s" % \
2074 self.schedule.numberOfSchedules
2076 for i, header in enumerate(self.schedule.headerArray):
2077 text = text + "\n\tschedule.headerArray[%d].type=%d" % \
2078 (i, header.type)
2079 text = text + "\n\tschedule.headerArray[%d].offset=%d" % \
2080 (i, header.offset)
2081 text = text + "\n\tschedule.dataArray[%d].slots[ " % i
2082 for slot in self.schedule.dataArray[i].slots:
2083 text = text + "0x%X " % slot
2084 text = text + "]"
2086 for dnstr in self.site_list:
2087 text = text + "\n\tsite_list=%s" % dnstr
2088 return text
2090 def load_sitelink(self, samdb):
2091 """Given a siteLink object with an prior initialization
2092 for the object's DN, search for the DN and load attributes
2093 from the samdb.
2095 attrs = [ "options",
2096 "systemFlags",
2097 "cost",
2098 "schedule",
2099 "replInterval",
2100 "siteList" ]
2101 try:
2102 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
2103 attrs=attrs, controls=['extended_dn:0'])
2105 except ldb.LdbError, (enum, estr):
2106 raise Exception("Unable to find SiteLink for (%s) - (%s)" %
2107 (self.dnstr, estr))
2109 msg = res[0]
2111 if "options" in msg:
2112 self.options = int(msg["options"][0])
2114 if "systemFlags" in msg:
2115 self.system_flags = int(msg["systemFlags"][0])
2117 if "cost" in msg:
2118 self.cost = int(msg["cost"][0])
2120 if "replInterval" in msg:
2121 self.interval = int(msg["replInterval"][0])
2123 if "siteList" in msg:
2124 for value in msg["siteList"]:
2125 dsdn = dsdb_Dn(samdb, value)
2126 guid = misc.GUID(dsdn.dn.get_extended_component('GUID'))
2127 if guid not in self.site_list:
2128 self.site_list.append(guid)
2130 class KCCFailedObject(object):
2131 def __init__(self, uuid, failure_count, time_first_failure, last_result, dns_name):
2132 self.uuid = uuid
2133 self.failure_count = failure_count
2134 self.time_first_failure = time_first_failure
2135 self.last_result = last_result
2136 self.dns_name = dns_name
2138 class VertexColor(object):
2139 (red, black, white, unknown) = range(0, 4)
2142 class Vertex(object):
2143 """Class encapsulation of a Site Vertex in the
2144 intersite topology replication algorithm
2146 def __init__(self, site, part):
2147 self.site = site
2148 self.part = part
2149 self.color = VertexColor.unknown
2150 self.edges = []
2151 self.accept_red_red = []
2152 self.accept_black = []
2153 self.repl_info = ReplInfo()
2154 self.root = self
2155 self.guid = None
2156 self.component_id = self
2157 self.demoted = False
2158 self.options = 0
2159 self.interval = 0
2161 def color_vertex(self):
2162 """Color each vertex to indicate which kind of NC
2163 replica it contains
2165 # IF s contains one or more DCs with full replicas of the
2166 # NC cr!nCName
2167 # SET v.Color to COLOR.RED
2168 # ELSEIF s contains one or more partial replicas of the NC
2169 # SET v.Color to COLOR.BLACK
2170 #ELSE
2171 # SET v.Color to COLOR.WHITE
2173 # set to minimum (no replica)
2174 self.color = VertexColor.white
2176 for dnstr, dsa in self.site.dsa_table.items():
2177 rep = dsa.get_current_replica(self.part.nc_dnstr)
2178 if rep is None:
2179 continue
2181 # We have a full replica which is the largest
2182 # value so exit
2183 if not rep.is_partial():
2184 self.color = VertexColor.red
2185 break
2186 else:
2187 self.color = VertexColor.black
2190 def is_red(self):
2191 assert(self.color != VertexColor.unknown)
2192 return (self.color == VertexColor.red)
2194 def is_black(self):
2195 assert(self.color != VertexColor.unknown)
2196 return (self.color == VertexColor.black)
2198 def is_white(self):
2199 assert(self.color != VertexColor.unknown)
2200 return (self.color == VertexColor.white)
2203 class IntersiteGraph(object):
2204 """Graph for representing the intersite"""
2205 def __init__(self):
2206 self.vertices = set()
2207 self.edges = set()
2208 self.edge_set = set()
2209 # All vertices that are endpoints of edges
2210 self.connected_vertices = None
2212 class MultiEdgeSet(object):
2213 """Defines a multi edge set"""
2214 def __init__(self):
2215 self.guid = 0 # objectGuid siteLinkBridge
2216 self.edges = []
2218 class MultiEdge(object):
2219 def __init__(self):
2220 self.site_link = None # object siteLink
2221 self.vertices = []
2222 self.con_type = None # interSiteTransport GUID
2223 self.repl_info = ReplInfo()
2224 self.directed = True
2226 class ReplInfo(object):
2227 def __init__(self):
2228 self.cost = 0
2229 self.interval = 0
2230 self.options = 0
2231 self.schedule = None
2233 class InternalEdge(object):
2234 def __init__(self, v1, v2, redred, repl, eType):
2235 self.v1 = v1
2236 self.v2 = v2
2237 self.red_red = redred
2238 self.repl_info = repl
2239 self.e_type = eType
2241 def __eq__(self, other):
2242 return not self < other and not other < self
2244 def __ne__(self, other):
2245 return self < other or other < self
2247 def __gt__(self, other):
2248 return other < self
2250 def __ge__(self, other):
2251 return not self < other
2253 def __le__(self, other):
2254 return not other < self
2256 # TODO compare options and interval
2257 def __lt__(self, other):
2258 if self.red_red != other.red_red:
2259 return self.red_red
2261 if self.repl_info.cost != other.repl_info.cost:
2262 return self.repl_info.cost < other.repl_info.cost
2264 self_time = total_schedule(self.repl_info.schedule)
2265 other_time = total_schedule(other.repl_info.schedule)
2266 if self_time != other_time:
2267 return self_time > other_time
2269 if self.v1.guid != other.v1.guid:
2270 return self.v1.guid < other.v1.guid
2272 if self.v2.guid != other.v2.guid:
2273 return self.v2.guid < other.v2.guid
2275 return self.e_type < other.e_type
2278 ##################################################
2279 # Global Functions and Variables
2280 ##################################################
2281 MAX_DWORD = 2 ** 32 - 1
2283 def sort_dsa_by_guid(dsa1, dsa2):
2284 return cmp(dsa1.dsa_guid, dsa2.dsa_guid)
2286 def total_schedule(schedule):
2287 if schedule is None:
2288 return 84 * 8 # 84 bytes = 84 * 8 bits
2290 total = 0
2291 for byte in schedule:
2292 while byte != 0:
2293 total += byte & 1
2294 byte >>= 1
2295 return total
2297 # Returns true if schedule intersect
2298 def combine_repl_info(info_a, info_b, info_c):
2299 info_c.interval = max(info_a.interval, info_b.interval)
2300 info_c.options = info_a.options & info_b.options
2302 if info_a.schedule is None:
2303 info_a.schedule = [0xFF] * 84
2304 if info_b.schedule is None:
2305 info_b.schedule = [0xFF] * 84
2307 new_info = [0] * 84
2308 i = 0
2309 count = 0
2310 while i < 84:
2311 # Note that this operation is actually bitwise
2312 new_info = info_a.schedule[i] & info_b.schedule[i]
2313 if new_info != 0:
2314 count += 1
2315 i += 1
2317 if count == 0:
2318 return False
2320 info_c.schedule = new_info
2322 # Truncate to MAX_DWORD
2323 info_c.cost = info_a.cost + info_b.cost
2324 if info_c.cost > MAX_DWORD:
2325 info_c.cost = MAX_DWORD
2327 return True
2329 def write_dot_file(basename, edge_list, label=None, destdir=None):
2330 from tempfile import NamedTemporaryFile
2331 if label:
2332 basename += '_' + label.translate(None, ', ') #fix DN, guid labels
2333 f = NamedTemporaryFile(suffix='.dot', prefix=basename + '_', delete=False, dir=destdir)
2334 graphname = ''.join(x for x in basename if x.isalnum())
2335 print >>f, 'graph %s {' % graphname
2336 print >>f, 'label="%s";\nfontsize=20' % (label or graphname)
2337 for a, b in edge_list:
2338 print >>f, '"%s" -- "%s"' % (a, b)
2339 print >>f, '}'
2342 f.close()