KCC: rearrange 3 functions in samba.kcc __init__
[Samba.git] / python / samba / kcc_utils.py
blob2e0be7f287c025b945bd0030d908a7b3fe2d3ab2
1 # KCC topology utilities
3 # Copyright (C) Dave Craft 2011
4 # Copyright (C) Jelmer Vernooij 2011
5 # Copyright (C) Andrew Bartlett 2015
7 # Andrew Bartlett's alleged work performed by his underlings Douglas
8 # Bagnall and Garming Sam.
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with this program. If not, see <http://www.gnu.org/licenses/>.
23 import ldb
24 import uuid
26 from samba import dsdb, unix2nttime
27 from samba.dcerpc import (
28 drsblobs,
29 drsuapi,
30 misc,
32 from samba.common import dsdb_Dn
33 from samba.ndr import (ndr_unpack, ndr_pack)
36 class KCCError(Exception):
37 pass
40 class NCType(object):
41 (unknown, schema, domain, config, application) = range(0, 5)
43 # map the NCType enum to strings for debugging
44 nctype_lut = {v: k for k, v in NCType.__dict__.items() if k[:2] != '__'}
47 class NamingContext(object):
48 """Base class for a naming context.
50 Holds the DN, GUID, SID (if available) and type of the DN.
51 Subclasses may inherit from this and specialize
52 """
54 def __init__(self, nc_dnstr):
55 """Instantiate a NamingContext
57 :param nc_dnstr: NC dn string
58 """
59 self.nc_dnstr = nc_dnstr
60 self.nc_guid = None
61 self.nc_sid = None
62 self.nc_type = NCType.unknown
64 def __str__(self):
65 '''Debug dump string output of class'''
66 text = "%s:" % (self.__class__.__name__,)
67 text = text + "\n\tnc_dnstr=%s" % self.nc_dnstr
68 text = text + "\n\tnc_guid=%s" % str(self.nc_guid)
70 if self.nc_sid is None:
71 text = text + "\n\tnc_sid=<absent>"
72 else:
73 text = text + "\n\tnc_sid=<present>"
75 text = text + "\n\tnc_type=%s (%s)" % (nctype_lut[self.nc_type],
76 self.nc_type)
77 return text
79 def load_nc(self, samdb):
80 attrs = ["objectGUID",
81 "objectSid"]
82 try:
83 res = samdb.search(base=self.nc_dnstr,
84 scope=ldb.SCOPE_BASE, attrs=attrs)
86 except ldb.LdbError, (enum, estr):
87 raise Exception("Unable to find naming context (%s) - (%s)" %
88 (self.nc_dnstr, estr))
89 msg = res[0]
90 if "objectGUID" in msg:
91 self.nc_guid = misc.GUID(samdb.schema_format_value("objectGUID",
92 msg["objectGUID"][0]))
93 if "objectSid" in msg:
94 self.nc_sid = msg["objectSid"][0]
96 assert self.nc_guid is not None
98 def is_schema(self):
99 '''Return True if NC is schema'''
100 assert self.nc_type != NCType.unknown
101 return self.nc_type == NCType.schema
103 def is_domain(self):
104 '''Return True if NC is domain'''
105 assert self.nc_type != NCType.unknown
106 return self.nc_type == NCType.domain
108 def is_application(self):
109 '''Return True if NC is application'''
110 assert self.nc_type != NCType.unknown
111 return self.nc_type == NCType.application
113 def is_config(self):
114 '''Return True if NC is config'''
115 assert self.nc_type != NCType.unknown
116 return self.nc_type == NCType.config
118 def identify_by_basedn(self, samdb):
119 """Given an NC object, identify what type is is thru
120 the samdb basedn strings and NC sid value
122 # Invoke loader to initialize guid and more
123 # importantly sid value (sid is used to identify
124 # domain NCs)
125 if self.nc_guid is None:
126 self.load_nc(samdb)
128 # We check against schema and config because they
129 # will be the same for all nTDSDSAs in the forest.
130 # That leaves the domain NCs which can be identified
131 # by sid and application NCs as the last identified
132 if self.nc_dnstr == str(samdb.get_schema_basedn()):
133 self.nc_type = NCType.schema
134 elif self.nc_dnstr == str(samdb.get_config_basedn()):
135 self.nc_type = NCType.config
136 elif self.nc_sid is not None:
137 self.nc_type = NCType.domain
138 else:
139 self.nc_type = NCType.application
141 def identify_by_dsa_attr(self, samdb, attr):
142 """Given an NC which has been discovered thru the
143 nTDSDSA database object, determine what type of NC
144 it is (i.e. schema, config, domain, application) via
145 the use of the schema attribute under which the NC
146 was found.
148 :param attr: attr of nTDSDSA object where NC DN appears
150 # If the NC is listed under msDS-HasDomainNCs then
151 # this can only be a domain NC and it is our default
152 # domain for this dsa
153 if attr == "msDS-HasDomainNCs":
154 self.nc_type = NCType.domain
156 # If the NC is listed under hasPartialReplicaNCs
157 # this is only a domain NC
158 elif attr == "hasPartialReplicaNCs":
159 self.nc_type = NCType.domain
161 # NCs listed under hasMasterNCs are either
162 # default domain, schema, or config. We
163 # utilize the identify_by_basedn() to
164 # identify those
165 elif attr == "hasMasterNCs":
166 self.identify_by_basedn(samdb)
168 # Still unknown (unlikely) but for completeness
169 # and for finally identifying application NCs
170 if self.nc_type == NCType.unknown:
171 self.identify_by_basedn(samdb)
174 class NCReplica(NamingContext):
175 """Naming context replica that is relative to a specific DSA.
177 This is a more specific form of NamingContext class (inheriting from that
178 class) and it identifies unique attributes of the DSA's replica for a NC.
181 def __init__(self, dsa_dnstr, dsa_guid, nc_dnstr):
182 """Instantiate a Naming Context Replica
184 :param dsa_guid: GUID of DSA where replica appears
185 :param nc_dnstr: NC dn string
187 self.rep_dsa_dnstr = dsa_dnstr
188 self.rep_dsa_guid = dsa_guid
189 self.rep_default = False # replica for DSA's default domain
190 self.rep_partial = False
191 self.rep_ro = False
192 self.rep_instantiated_flags = 0
194 self.rep_fsmo_role_owner = None
196 # RepsFromTo tuples
197 self.rep_repsFrom = []
199 # The (is present) test is a combination of being
200 # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or
201 # hasPartialReplicaNCs) as well as its replica flags found
202 # thru the msDS-HasInstantiatedNCs. If the NC replica meets
203 # the first enumeration test then this flag is set true
204 self.rep_present_criteria_one = False
206 # Call my super class we inherited from
207 NamingContext.__init__(self, nc_dnstr)
209 def __str__(self):
210 '''Debug dump string output of class'''
211 text = "%s:" % self.__class__.__name__
212 text = text + "\n\tdsa_dnstr=%s" % self.rep_dsa_dnstr
213 text = text + "\n\tdsa_guid=%s" % self.rep_dsa_guid
214 text = text + "\n\tdefault=%s" % self.rep_default
215 text = text + "\n\tro=%s" % self.rep_ro
216 text = text + "\n\tpartial=%s" % self.rep_partial
217 text = text + "\n\tpresent=%s" % self.is_present()
218 text = text + "\n\tfsmo_role_owner=%s" % self.rep_fsmo_role_owner
220 for rep in self.rep_repsFrom:
221 text = text + "\n%s" % rep
223 return "%s\n%s" % (NamingContext.__str__(self), text)
225 def set_instantiated_flags(self, flags=None):
226 '''Set or clear NC replica instantiated flags'''
227 if flags is None:
228 self.rep_instantiated_flags = 0
229 else:
230 self.rep_instantiated_flags = flags
232 def identify_by_dsa_attr(self, samdb, attr):
233 """Given an NC which has been discovered thru the
234 nTDSDSA database object, determine what type of NC
235 replica it is (i.e. partial, read only, default)
237 :param attr: attr of nTDSDSA object where NC DN appears
239 # If the NC was found under hasPartialReplicaNCs
240 # then a partial replica at this dsa
241 if attr == "hasPartialReplicaNCs":
242 self.rep_partial = True
243 self.rep_present_criteria_one = True
245 # If the NC is listed under msDS-HasDomainNCs then
246 # this can only be a domain NC and it is the DSA's
247 # default domain NC
248 elif attr == "msDS-HasDomainNCs":
249 self.rep_default = True
251 # NCs listed under hasMasterNCs are either
252 # default domain, schema, or config. We check
253 # against schema and config because they will be
254 # the same for all nTDSDSAs in the forest. That
255 # leaves the default domain NC remaining which
256 # may be different for each nTDSDSAs (and thus
257 # we don't compare agains this samdb's default
258 # basedn
259 elif attr == "hasMasterNCs":
260 self.rep_present_criteria_one = True
262 if self.nc_dnstr != str(samdb.get_schema_basedn()) and \
263 self.nc_dnstr != str(samdb.get_config_basedn()):
264 self.rep_default = True
266 # RODC only
267 elif attr == "msDS-hasFullReplicaNCs":
268 self.rep_present_criteria_one = True
269 self.rep_ro = True
271 # Not RODC
272 elif attr == "msDS-hasMasterNCs":
273 self.rep_present_criteria_one = True
274 self.rep_ro = False
276 # Now use this DSA attribute to identify the naming
277 # context type by calling the super class method
278 # of the same name
279 NamingContext.identify_by_dsa_attr(self, samdb, attr)
281 def is_default(self):
282 """Whether this is a default domain for the dsa that this NC appears on
284 return self.rep_default
286 def is_ro(self):
287 '''Return True if NC replica is read only'''
288 return self.rep_ro
290 def is_partial(self):
291 '''Return True if NC replica is partial'''
292 return self.rep_partial
294 def is_present(self):
295 """Given an NC replica which has been discovered thru the
296 nTDSDSA database object and populated with replica flags
297 from the msDS-HasInstantiatedNCs; return whether the NC
298 replica is present (true) or if the IT_NC_GOING flag is
299 set then the NC replica is not present (false)
301 if self.rep_present_criteria_one and \
302 self.rep_instantiated_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0:
303 return True
304 return False
306 def load_repsFrom(self, samdb):
307 """Given an NC replica which has been discovered thru the nTDSDSA
308 database object, load the repsFrom attribute for the local replica.
309 held by my dsa. The repsFrom attribute is not replicated so this
310 attribute is relative only to the local DSA that the samdb exists on
312 try:
313 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
314 attrs=["repsFrom"])
316 except ldb.LdbError, (enum, estr):
317 raise Exception("Unable to find NC for (%s) - (%s)" %
318 (self.nc_dnstr, estr))
320 msg = res[0]
322 # Possibly no repsFrom if this is a singleton DC
323 if "repsFrom" in msg:
324 for value in msg["repsFrom"]:
325 rep = RepsFromTo(self.nc_dnstr,
326 ndr_unpack(drsblobs.repsFromToBlob, value))
327 self.rep_repsFrom.append(rep)
329 def commit_repsFrom(self, samdb, ro=False):
330 """Commit repsFrom to the database"""
332 # XXX - This is not truly correct according to the MS-TECH
333 # docs. To commit a repsFrom we should be using RPCs
334 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
335 # IDL_DRSReplicaDel to affect a repsFrom change.
337 # Those RPCs are missing in samba, so I'll have to
338 # implement them to get this to more accurately
339 # reflect the reference docs. As of right now this
340 # commit to the database will work as its what the
341 # older KCC also did
342 modify = False
343 newreps = []
344 delreps = []
346 for repsFrom in self.rep_repsFrom:
348 # Leave out any to be deleted from
349 # replacement list. Build a list
350 # of to be deleted reps which we will
351 # remove from rep_repsFrom list below
352 if repsFrom.to_be_deleted:
353 delreps.append(repsFrom)
354 modify = True
355 continue
357 if repsFrom.is_modified():
358 repsFrom.set_unmodified()
359 modify = True
361 # current (unmodified) elements also get
362 # appended here but no changes will occur
363 # unless something is "to be modified" or
364 # "to be deleted"
365 newreps.append(ndr_pack(repsFrom.ndr_blob))
367 # Now delete these from our list of rep_repsFrom
368 for repsFrom in delreps:
369 self.rep_repsFrom.remove(repsFrom)
370 delreps = []
372 # Nothing to do if no reps have been modified or
373 # need to be deleted or input option has informed
374 # us to be "readonly" (ro). Leave database
375 # record "as is"
376 if not modify or ro:
377 return
379 m = ldb.Message()
380 m.dn = ldb.Dn(samdb, self.nc_dnstr)
382 m["repsFrom"] = \
383 ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsFrom")
385 try:
386 samdb.modify(m)
388 except ldb.LdbError, estr:
389 raise Exception("Could not set repsFrom for (%s) - (%s)" %
390 (self.nc_dnstr, estr))
392 def load_replUpToDateVector(self, samdb):
393 """Given an NC replica which has been discovered thru the nTDSDSA
394 database object, load the replUpToDateVector attribute for the
395 local replica. held by my dsa. The replUpToDateVector
396 attribute is not replicated so this attribute is relative only
397 to the local DSA that the samdb exists on
400 try:
401 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
402 attrs=["replUpToDateVector"])
404 except ldb.LdbError, (enum, estr):
405 raise Exception("Unable to find NC for (%s) - (%s)" %
406 (self.nc_dnstr, estr))
408 msg = res[0]
410 # Possibly no replUpToDateVector if this is a singleton DC
411 if "replUpToDateVector" in msg:
412 value = msg["replUpToDateVector"][0]
413 blob = ndr_unpack(drsblobs.replUpToDateVectorBlob,
414 value)
415 if blob.version != 2:
416 # Samba only generates version 2, and this runs locally
417 raise AttributeError("Unexpected replUpToDateVector version %d"
418 % blob.version)
420 self.rep_replUpToDateVector_cursors = blob.ctr.cursors
421 else:
422 self.rep_replUpToDateVector_cursors = []
424 def dumpstr_to_be_deleted(self):
425 return '\n'.join(str(x) for x in self.rep_repsFrom if x.to_be_deleted)
427 def dumpstr_to_be_modified(self):
428 return '\n'.join(str(x) for x in self.rep_repsFrom if x.is_modified())
430 def load_fsmo_roles(self, samdb):
431 """Given an NC replica which has been discovered thru the nTDSDSA
432 database object, load the fSMORoleOwner attribute.
434 try:
435 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
436 attrs=["fSMORoleOwner"])
438 except ldb.LdbError, (enum, estr):
439 raise Exception("Unable to find NC for (%s) - (%s)" %
440 (self.nc_dnstr, estr))
442 msg = res[0]
444 # Possibly no fSMORoleOwner
445 if "fSMORoleOwner" in msg:
446 self.rep_fsmo_role_owner = msg["fSMORoleOwner"]
448 def is_fsmo_role_owner(self, dsa_dnstr):
449 if self.rep_fsmo_role_owner is not None and \
450 self.rep_fsmo_role_owner == dsa_dnstr:
451 return True
452 return False
455 class DirectoryServiceAgent(object):
457 def __init__(self, dsa_dnstr):
458 """Initialize DSA class.
460 Class is subsequently fully populated by calling the load_dsa() method
462 :param dsa_dnstr: DN of the nTDSDSA
464 self.dsa_dnstr = dsa_dnstr
465 self.dsa_guid = None
466 self.dsa_ivid = None
467 self.dsa_is_ro = False
468 self.dsa_is_istg = False
469 self.dsa_options = 0
470 self.dsa_behavior = 0
471 self.default_dnstr = None # default domain dn string for dsa
473 # NCReplicas for this dsa that are "present"
474 # Indexed by DN string of naming context
475 self.current_rep_table = {}
477 # NCReplicas for this dsa that "should be present"
478 # Indexed by DN string of naming context
479 self.needed_rep_table = {}
481 # NTDSConnections for this dsa. These are current
482 # valid connections that are committed or pending a commit
483 # in the database. Indexed by DN string of connection
484 self.connect_table = {}
486 def __str__(self):
487 '''Debug dump string output of class'''
489 text = "%s:" % self.__class__.__name__
490 if self.dsa_dnstr is not None:
491 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
492 if self.dsa_guid is not None:
493 text = text + "\n\tdsa_guid=%s" % str(self.dsa_guid)
494 if self.dsa_ivid is not None:
495 text = text + "\n\tdsa_ivid=%s" % str(self.dsa_ivid)
497 text = text + "\n\tro=%s" % self.is_ro()
498 text = text + "\n\tgc=%s" % self.is_gc()
499 text = text + "\n\tistg=%s" % self.is_istg()
501 text = text + "\ncurrent_replica_table:"
502 text = text + "\n%s" % self.dumpstr_current_replica_table()
503 text = text + "\nneeded_replica_table:"
504 text = text + "\n%s" % self.dumpstr_needed_replica_table()
505 text = text + "\nconnect_table:"
506 text = text + "\n%s" % self.dumpstr_connect_table()
508 return text
510 def get_current_replica(self, nc_dnstr):
511 return self.current_rep_table.get(nc_dnstr)
513 def is_istg(self):
514 '''Returns True if dsa is intersite topology generator for it's site'''
515 # The KCC on an RODC always acts as an ISTG for itself
516 return self.dsa_is_istg or self.dsa_is_ro
518 def is_ro(self):
519 '''Returns True if dsa a read only domain controller'''
520 return self.dsa_is_ro
522 def is_gc(self):
523 '''Returns True if dsa hosts a global catalog'''
524 if (self.options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
525 return True
526 return False
528 def is_minimum_behavior(self, version):
529 """Is dsa at minimum windows level greater than or equal to (version)
531 :param version: Windows version to test against
532 (e.g. DS_DOMAIN_FUNCTION_2008)
534 if self.dsa_behavior >= version:
535 return True
536 return False
538 def is_translate_ntdsconn_disabled(self):
539 """Whether this allows NTDSConnection translation in its options."""
540 if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0:
541 return True
542 return False
544 def get_rep_tables(self):
545 """Return DSA current and needed replica tables
547 return self.current_rep_table, self.needed_rep_table
549 def get_parent_dnstr(self):
550 """Get the parent DN string of this object."""
551 head, sep, tail = self.dsa_dnstr.partition(',')
552 return tail
554 def load_dsa(self, samdb):
555 """Load a DSA from the samdb.
557 Prior initialization has given us the DN of the DSA that we are to
558 load. This method initializes all other attributes, including loading
559 the NC replica table for this DSA.
561 attrs = ["objectGUID",
562 "invocationID",
563 "options",
564 "msDS-isRODC",
565 "msDS-Behavior-Version"]
566 try:
567 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
568 attrs=attrs)
570 except ldb.LdbError, (enum, estr):
571 raise Exception("Unable to find nTDSDSA for (%s) - (%s)" %
572 (self.dsa_dnstr, estr))
574 msg = res[0]
575 self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID",
576 msg["objectGUID"][0]))
578 # RODCs don't originate changes and thus have no invocationId,
579 # therefore we must check for existence first
580 if "invocationId" in msg:
581 self.dsa_ivid = misc.GUID(samdb.schema_format_value("objectGUID",
582 msg["invocationId"][0]))
584 if "options" in msg:
585 self.options = int(msg["options"][0])
587 if "msDS-isRODC" in msg and msg["msDS-isRODC"][0] == "TRUE":
588 self.dsa_is_ro = True
589 else:
590 self.dsa_is_ro = False
592 if "msDS-Behavior-Version" in msg:
593 self.dsa_behavior = int(msg['msDS-Behavior-Version'][0])
595 # Load the NC replicas that are enumerated on this dsa
596 self.load_current_replica_table(samdb)
598 # Load the nTDSConnection that are enumerated on this dsa
599 self.load_connection_table(samdb)
601 def load_current_replica_table(self, samdb):
602 """Method to load the NC replica's listed for DSA object.
604 This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs,
605 hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, and
606 msDS-HasInstantiatedNCs) to determine complete list of NC replicas that
607 are enumerated for the DSA. Once a NC replica is loaded it is
608 identified (schema, config, etc) and the other replica attributes
609 (partial, ro, etc) are determined.
611 :param samdb: database to query for DSA replica list
613 ncattrs = [
614 # not RODC - default, config, schema (old style)
615 "hasMasterNCs",
616 # not RODC - default, config, schema, app NCs
617 "msDS-hasMasterNCs",
618 # domain NC partial replicas
619 "hasPartialReplicaNCs",
620 # default domain NC
621 "msDS-HasDomainNCs",
622 # RODC only - default, config, schema, app NCs
623 "msDS-hasFullReplicaNCs",
624 # Identifies if replica is coming, going, or stable
625 "msDS-HasInstantiatedNCs"
627 try:
628 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
629 attrs=ncattrs)
631 except ldb.LdbError, (enum, estr):
632 raise Exception("Unable to find nTDSDSA NCs for (%s) - (%s)" %
633 (self.dsa_dnstr, estr))
635 # The table of NCs for the dsa we are searching
636 tmp_table = {}
638 # We should get one response to our query here for
639 # the ntds that we requested
640 if len(res[0]) > 0:
642 # Our response will contain a number of elements including
643 # the dn of the dsa as well as elements for each
644 # attribute (e.g. hasMasterNCs). Each of these elements
645 # is a dictonary list which we retrieve the keys for and
646 # then iterate over them
647 for k in res[0].keys():
648 if k == "dn":
649 continue
651 # For each attribute type there will be one or more DNs
652 # listed. For instance DCs normally have 3 hasMasterNCs
653 # listed.
654 for value in res[0][k]:
655 # Turn dn into a dsdb_Dn so we can use
656 # its methods to parse a binary DN
657 dsdn = dsdb_Dn(samdb, value)
658 flags = dsdn.get_binary_integer()
659 dnstr = str(dsdn.dn)
661 if not dnstr in tmp_table:
662 rep = NCReplica(self.dsa_dnstr, self.dsa_guid, dnstr)
663 tmp_table[dnstr] = rep
664 else:
665 rep = tmp_table[dnstr]
667 if k == "msDS-HasInstantiatedNCs":
668 rep.set_instantiated_flags(flags)
669 continue
671 rep.identify_by_dsa_attr(samdb, k)
673 # if we've identified the default domain NC
674 # then save its DN string
675 if rep.is_default():
676 self.default_dnstr = dnstr
677 else:
678 raise Exception("No nTDSDSA NCs for (%s)" % self.dsa_dnstr)
680 # Assign our newly built NC replica table to this dsa
681 self.current_rep_table = tmp_table
683 def add_needed_replica(self, rep):
684 """Method to add a NC replica that "should be present" to the
685 needed_rep_table.
687 self.needed_rep_table[rep.nc_dnstr] = rep
689 def load_connection_table(self, samdb):
690 """Method to load the nTDSConnections listed for DSA object.
692 :param samdb: database to query for DSA connection list
694 try:
695 res = samdb.search(base=self.dsa_dnstr,
696 scope=ldb.SCOPE_SUBTREE,
697 expression="(objectClass=nTDSConnection)")
699 except ldb.LdbError, (enum, estr):
700 raise Exception("Unable to find nTDSConnection for (%s) - (%s)" %
701 (self.dsa_dnstr, estr))
703 for msg in res:
704 dnstr = str(msg.dn)
706 # already loaded
707 if dnstr in self.connect_table:
708 continue
710 connect = NTDSConnection(dnstr)
712 connect.load_connection(samdb)
713 self.connect_table[dnstr] = connect
715 def commit_connections(self, samdb, ro=False):
716 """Method to commit any uncommitted nTDSConnections
717 modifications that are in our table. These would be
718 identified connections that are marked to be added or
719 deleted
721 :param samdb: database to commit DSA connection list to
722 :param ro: if (true) then peform internal operations but
723 do not write to the database (readonly)
725 delconn = []
727 for dnstr, connect in self.connect_table.items():
728 if connect.to_be_added:
729 connect.commit_added(samdb, ro)
731 if connect.to_be_modified:
732 connect.commit_modified(samdb, ro)
734 if connect.to_be_deleted:
735 connect.commit_deleted(samdb, ro)
736 delconn.append(dnstr)
738 # Now delete the connection from the table
739 for dnstr in delconn:
740 del self.connect_table[dnstr]
742 def add_connection(self, dnstr, connect):
743 assert dnstr not in self.connect_table
744 self.connect_table[dnstr] = connect
746 def get_connection_by_from_dnstr(self, from_dnstr):
747 """Scan DSA nTDSConnection table and return connection
748 with a "fromServer" dn string equivalent to method
749 input parameter.
751 :param from_dnstr: search for this from server entry
753 answer = []
754 for connect in self.connect_table.values():
755 if connect.get_from_dnstr() == from_dnstr:
756 answer.append(connect)
758 return answer
760 def dumpstr_current_replica_table(self):
761 '''Debug dump string output of current replica table'''
762 return '\n'.join(str(x) for x in self.current_rep_table)
764 def dumpstr_needed_replica_table(self):
765 '''Debug dump string output of needed replica table'''
766 return '\n'.join(str(x) for x in self.needed_rep_table)
768 def dumpstr_connect_table(self):
769 '''Debug dump string output of connect table'''
770 return '\n'.join(str(x) for x in self.connect_table)
772 def new_connection(self, options, flags, transport, from_dnstr, sched):
773 """Set up a new connection for the DSA based on input
774 parameters. Connection will be added to the DSA
775 connect_table and will be marked as "to be added" pending
776 a call to commit_connections()
778 dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
780 connect = NTDSConnection(dnstr)
781 connect.to_be_added = True
782 connect.enabled = True
783 connect.from_dnstr = from_dnstr
784 connect.options = options
785 connect.flags = flags
787 if transport is not None:
788 connect.transport_dnstr = transport.dnstr
789 connect.transport_guid = transport.guid
791 if sched is not None:
792 connect.schedule = sched
793 else:
794 # Create schedule. Attribute valuse set according to MS-TECH
795 # intrasite connection creation document
796 connect.schedule = new_connection_schedule()
798 self.add_connection(dnstr, connect)
799 return connect
802 class NTDSConnection(object):
803 """Class defines a nTDSConnection found under a DSA
805 def __init__(self, dnstr):
806 self.dnstr = dnstr
807 self.guid = None
808 self.enabled = False
809 self.whenCreated = 0
810 self.to_be_added = False # new connection needs to be added
811 self.to_be_deleted = False # old connection needs to be deleted
812 self.to_be_modified = False
813 self.options = 0
814 self.system_flags = 0
815 self.transport_dnstr = None
816 self.transport_guid = None
817 self.from_dnstr = None
818 self.schedule = None
820 def __str__(self):
821 '''Debug dump string output of NTDSConnection object'''
823 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
824 text = text + "\n\tenabled=%s" % self.enabled
825 text = text + "\n\tto_be_added=%s" % self.to_be_added
826 text = text + "\n\tto_be_deleted=%s" % self.to_be_deleted
827 text = text + "\n\tto_be_modified=%s" % self.to_be_modified
828 text = text + "\n\toptions=0x%08X" % self.options
829 text = text + "\n\tsystem_flags=0x%08X" % self.system_flags
830 text = text + "\n\twhenCreated=%d" % self.whenCreated
831 text = text + "\n\ttransport_dn=%s" % self.transport_dnstr
833 if self.guid is not None:
834 text = text + "\n\tguid=%s" % str(self.guid)
836 if self.transport_guid is not None:
837 text = text + "\n\ttransport_guid=%s" % str(self.transport_guid)
839 text = text + "\n\tfrom_dn=%s" % self.from_dnstr
841 if self.schedule is not None:
842 text += "\n\tschedule.size=%s" % self.schedule.size
843 text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
844 text += ("\n\tschedule.numberOfSchedules=%s" %
845 self.schedule.numberOfSchedules)
847 for i, header in enumerate(self.schedule.headerArray):
848 text += ("\n\tschedule.headerArray[%d].type=%d" %
849 (i, header.type))
850 text += ("\n\tschedule.headerArray[%d].offset=%d" %
851 (i, header.offset))
852 text += "\n\tschedule.dataArray[%d].slots[ " % i
853 for slot in self.schedule.dataArray[i].slots:
854 text = text + "0x%X " % slot
855 text = text + "]"
857 return text
859 def load_connection(self, samdb):
860 """Given a NTDSConnection object with an prior initialization
861 for the object's DN, search for the DN and load attributes
862 from the samdb.
864 attrs = ["options",
865 "enabledConnection",
866 "schedule",
867 "whenCreated",
868 "objectGUID",
869 "transportType",
870 "fromServer",
871 "systemFlags"]
872 try:
873 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
874 attrs=attrs)
876 except ldb.LdbError, (enum, estr):
877 raise Exception("Unable to find nTDSConnection for (%s) - (%s)" %
878 (self.dnstr, estr))
880 msg = res[0]
882 if "options" in msg:
883 self.options = int(msg["options"][0])
885 if "enabledConnection" in msg:
886 if msg["enabledConnection"][0].upper().lstrip().rstrip() == "TRUE":
887 self.enabled = True
889 if "systemFlags" in msg:
890 self.system_flags = int(msg["systemFlags"][0])
892 if "objectGUID" in msg:
893 self.guid = \
894 misc.GUID(samdb.schema_format_value("objectGUID",
895 msg["objectGUID"][0]))
897 if "transportType" in msg:
898 dsdn = dsdb_Dn(samdb, msg["transportType"][0])
899 self.load_connection_transport(samdb, str(dsdn.dn))
901 if "schedule" in msg:
902 self.schedule = ndr_unpack(drsblobs.schedule, msg["schedule"][0])
904 if "whenCreated" in msg:
905 self.whenCreated = ldb.string_to_time(msg["whenCreated"][0])
907 if "fromServer" in msg:
908 dsdn = dsdb_Dn(samdb, msg["fromServer"][0])
909 self.from_dnstr = str(dsdn.dn)
910 assert self.from_dnstr is not None
912 def load_connection_transport(self, samdb, tdnstr):
913 """Given a NTDSConnection object which enumerates a transport
914 DN, load the transport information for the connection object
916 :param tdnstr: transport DN to load
918 attrs = ["objectGUID"]
919 try:
920 res = samdb.search(base=tdnstr,
921 scope=ldb.SCOPE_BASE, attrs=attrs)
923 except ldb.LdbError, (enum, estr):
924 raise Exception("Unable to find transport (%s) - (%s)" %
925 (tdnstr, estr))
927 if "objectGUID" in res[0]:
928 msg = res[0]
929 self.transport_dnstr = tdnstr
930 self.transport_guid = \
931 misc.GUID(samdb.schema_format_value("objectGUID",
932 msg["objectGUID"][0]))
933 assert self.transport_dnstr is not None
934 assert self.transport_guid is not None
936 def commit_deleted(self, samdb, ro=False):
937 """Local helper routine for commit_connections() which
938 handles committed connections that are to be deleted from
939 the database database
941 assert self.to_be_deleted
942 self.to_be_deleted = False
944 # No database modification requested
945 if ro:
946 return
948 try:
949 samdb.delete(self.dnstr)
950 except ldb.LdbError, (enum, estr):
951 raise Exception("Could not delete nTDSConnection for (%s) - (%s)" %
952 (self.dnstr, estr))
954 def commit_added(self, samdb, ro=False):
955 """Local helper routine for commit_connections() which
956 handles committed connections that are to be added to the
957 database
959 assert self.to_be_added
960 self.to_be_added = False
962 # No database modification requested
963 if ro:
964 return
966 # First verify we don't have this entry to ensure nothing
967 # is programatically amiss
968 found = False
969 try:
970 msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
971 if len(msg) != 0:
972 found = True
974 except ldb.LdbError, (enum, estr):
975 if enum != ldb.ERR_NO_SUCH_OBJECT:
976 raise Exception("Unable to search for (%s) - (%s)" %
977 (self.dnstr, estr))
978 if found:
979 raise Exception("nTDSConnection for (%s) already exists!" %
980 self.dnstr)
982 if self.enabled:
983 enablestr = "TRUE"
984 else:
985 enablestr = "FALSE"
987 # Prepare a message for adding to the samdb
988 m = ldb.Message()
989 m.dn = ldb.Dn(samdb, self.dnstr)
991 m["objectClass"] = \
992 ldb.MessageElement("nTDSConnection", ldb.FLAG_MOD_ADD,
993 "objectClass")
994 m["showInAdvancedViewOnly"] = \
995 ldb.MessageElement("TRUE", ldb.FLAG_MOD_ADD,
996 "showInAdvancedViewOnly")
997 m["enabledConnection"] = \
998 ldb.MessageElement(enablestr, ldb.FLAG_MOD_ADD,
999 "enabledConnection")
1000 m["fromServer"] = \
1001 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_ADD, "fromServer")
1002 m["options"] = \
1003 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options")
1004 m["systemFlags"] = \
1005 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_ADD,
1006 "systemFlags")
1008 if self.transport_dnstr is not None:
1009 m["transportType"] = \
1010 ldb.MessageElement(str(self.transport_dnstr), ldb.FLAG_MOD_ADD,
1011 "transportType")
1013 if self.schedule is not None:
1014 m["schedule"] = \
1015 ldb.MessageElement(ndr_pack(self.schedule),
1016 ldb.FLAG_MOD_ADD, "schedule")
1017 try:
1018 samdb.add(m)
1019 except ldb.LdbError, (enum, estr):
1020 raise Exception("Could not add nTDSConnection for (%s) - (%s)" %
1021 (self.dnstr, estr))
1023 def commit_modified(self, samdb, ro=False):
1024 """Local helper routine for commit_connections() which
1025 handles committed connections that are to be modified to the
1026 database
1028 assert self.to_be_modified
1029 self.to_be_modified = False
1031 # No database modification requested
1032 if ro:
1033 return
1035 # First verify we have this entry to ensure nothing
1036 # is programatically amiss
1037 try:
1038 #XXX msg is never used
1039 msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1040 found = True
1042 except ldb.LdbError, (enum, estr):
1043 if enum == ldb.ERR_NO_SUCH_OBJECT:
1044 found = False
1045 else:
1046 raise Exception("Unable to search for (%s) - (%s)" %
1047 (self.dnstr, estr))
1048 if not found:
1049 raise Exception("nTDSConnection for (%s) doesn't exist!" %
1050 self.dnstr)
1052 if self.enabled:
1053 enablestr = "TRUE"
1054 else:
1055 enablestr = "FALSE"
1057 # Prepare a message for modifying the samdb
1058 m = ldb.Message()
1059 m.dn = ldb.Dn(samdb, self.dnstr)
1061 m["enabledConnection"] = \
1062 ldb.MessageElement(enablestr, ldb.FLAG_MOD_REPLACE,
1063 "enabledConnection")
1064 m["fromServer"] = \
1065 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_REPLACE,
1066 "fromServer")
1067 m["options"] = \
1068 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_REPLACE,
1069 "options")
1070 m["systemFlags"] = \
1071 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_REPLACE,
1072 "systemFlags")
1074 if self.transport_dnstr is not None:
1075 m["transportType"] = \
1076 ldb.MessageElement(str(self.transport_dnstr),
1077 ldb.FLAG_MOD_REPLACE, "transportType")
1078 else:
1079 m["transportType"] = \
1080 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "transportType")
1082 if self.schedule is not None:
1083 m["schedule"] = \
1084 ldb.MessageElement(ndr_pack(self.schedule),
1085 ldb.FLAG_MOD_REPLACE, "schedule")
1086 else:
1087 m["schedule"] = \
1088 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "schedule")
1089 try:
1090 samdb.modify(m)
1091 except ldb.LdbError, (enum, estr):
1092 raise Exception("Could not modify nTDSConnection for (%s) - (%s)" %
1093 (self.dnstr, estr))
1095 def set_modified(self, truefalse):
1096 self.to_be_modified = truefalse
1098 def set_added(self, truefalse):
1099 self.to_be_added = truefalse
1101 def set_deleted(self, truefalse):
1102 self.to_be_deleted = truefalse
1104 def is_schedule_minimum_once_per_week(self):
1105 """Returns True if our schedule includes at least one
1106 replication interval within the week. False otherwise
1108 # replinfo schedule is None means "always", while
1109 # NTDSConnection schedule is None means "never".
1110 if self.schedule is None or self.schedule.dataArray[0] is None:
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 is_rodc_topology(self):
1151 """Returns True if NTDS Connection specifies RODC
1152 topology only
1154 if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
1155 return False
1156 return True
1158 def is_generated(self):
1159 """Returns True if NTDS Connection was generated by the
1160 KCC topology algorithm as opposed to set by the administrator
1162 if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0:
1163 return False
1164 return True
1166 def is_override_notify_default(self):
1167 """Returns True if NTDS Connection should override notify default
1169 if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0:
1170 return False
1171 return True
1173 def is_use_notify(self):
1174 """Returns True if NTDS Connection should use notify
1176 if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0:
1177 return False
1178 return True
1180 def is_twoway_sync(self):
1181 """Returns True if NTDS Connection should use twoway sync
1183 if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0:
1184 return False
1185 return True
1187 def is_intersite_compression_disabled(self):
1188 """Returns True if NTDS Connection intersite compression
1189 is disabled
1191 if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0:
1192 return False
1193 return True
1195 def is_user_owned_schedule(self):
1196 """Returns True if NTDS Connection has a user owned schedule
1198 if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0:
1199 return False
1200 return True
1202 def is_enabled(self):
1203 """Returns True if NTDS Connection is enabled
1205 return self.enabled
1207 def get_from_dnstr(self):
1208 '''Return fromServer dn string attribute'''
1209 return self.from_dnstr
1212 class Partition(NamingContext):
1213 """A naming context discovered thru Partitions DN of the config schema.
1215 This is a more specific form of NamingContext class (inheriting from that
1216 class) and it identifies unique attributes enumerated in the Partitions
1217 such as which nTDSDSAs are cross referenced for replicas
1219 def __init__(self, partstr):
1220 self.partstr = partstr
1221 self.enabled = True
1222 self.system_flags = 0
1223 self.rw_location_list = []
1224 self.ro_location_list = []
1226 # We don't have enough info to properly
1227 # fill in the naming context yet. We'll get that
1228 # fully set up with load_partition().
1229 NamingContext.__init__(self, None)
1231 def load_partition(self, samdb):
1232 """Given a Partition class object that has been initialized with its
1233 partition dn string, load the partition from the sam database, identify
1234 the type of the partition (schema, domain, etc) and record the list of
1235 nTDSDSAs that appear in the cross reference attributes
1236 msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations.
1238 :param samdb: sam database to load partition from
1240 attrs = ["nCName",
1241 "Enabled",
1242 "systemFlags",
1243 "msDS-NC-Replica-Locations",
1244 "msDS-NC-RO-Replica-Locations"]
1245 try:
1246 res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE,
1247 attrs=attrs)
1249 except ldb.LdbError, (enum, estr):
1250 raise Exception("Unable to find partition for (%s) - (%s)" % (
1251 self.partstr, estr))
1253 msg = res[0]
1254 for k in msg.keys():
1255 if k == "dn":
1256 continue
1258 if k == "Enabled":
1259 if msg[k][0].upper().lstrip().rstrip() == "TRUE":
1260 self.enabled = True
1261 else:
1262 self.enabled = False
1263 continue
1265 if k == "systemFlags":
1266 self.system_flags = int(msg[k][0])
1267 continue
1269 for value in msg[k]:
1270 dsdn = dsdb_Dn(samdb, value)
1271 dnstr = str(dsdn.dn)
1273 if k == "nCName":
1274 self.nc_dnstr = dnstr
1275 continue
1277 if k == "msDS-NC-Replica-Locations":
1278 self.rw_location_list.append(dnstr)
1279 continue
1281 if k == "msDS-NC-RO-Replica-Locations":
1282 self.ro_location_list.append(dnstr)
1283 continue
1285 # Now identify what type of NC this partition
1286 # enumerated
1287 self.identify_by_basedn(samdb)
1289 def is_enabled(self):
1290 """Returns True if partition is enabled
1292 return self.is_enabled
1294 def is_foreign(self):
1295 """Returns True if this is not an Active Directory NC in our
1296 forest but is instead something else (e.g. a foreign NC)
1298 if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0:
1299 return True
1300 else:
1301 return False
1303 def should_be_present(self, target_dsa):
1304 """Tests whether this partition should have an NC replica
1305 on the target dsa. This method returns a tuple of
1306 needed=True/False, ro=True/False, partial=True/False
1308 :param target_dsa: should NC be present on target dsa
1310 ro = False
1311 partial = False
1313 # If this is the config, schema, or default
1314 # domain NC for the target dsa then it should
1315 # be present
1316 needed = (self.nc_type == NCType.config or
1317 self.nc_type == NCType.schema or
1318 (self.nc_type == NCType.domain and
1319 self.nc_dnstr == target_dsa.default_dnstr))
1321 # A writable replica of an application NC should be present
1322 # if there a cross reference to the target DSA exists. Depending
1323 # on whether the DSA is ro we examine which type of cross reference
1324 # to look for (msDS-NC-Replica-Locations or
1325 # msDS-NC-RO-Replica-Locations
1326 if self.nc_type == NCType.application:
1327 if target_dsa.is_ro():
1328 if target_dsa.dsa_dnstr in self.ro_location_list:
1329 needed = True
1330 else:
1331 if target_dsa.dsa_dnstr in self.rw_location_list:
1332 needed = True
1334 # If the target dsa is a gc then a partial replica of a
1335 # domain NC (other than the DSAs default domain) should exist
1336 # if there is also a cross reference for the DSA
1337 if (target_dsa.is_gc() and
1338 self.nc_type == NCType.domain and
1339 self.nc_dnstr != target_dsa.default_dnstr and
1340 (target_dsa.dsa_dnstr in self.ro_location_list or
1341 target_dsa.dsa_dnstr in self.rw_location_list)):
1342 needed = True
1343 partial = True
1345 # partial NCs are always readonly
1346 if needed and (target_dsa.is_ro() or partial):
1347 ro = True
1349 return needed, ro, partial
1351 def __str__(self):
1352 '''Debug dump string output of class'''
1353 text = "%s" % NamingContext.__str__(self)
1354 text = text + "\n\tpartdn=%s" % self.partstr
1355 for k in self.rw_location_list:
1356 text = text + "\n\tmsDS-NC-Replica-Locations=%s" % k
1357 for k in self.ro_location_list:
1358 text = text + "\n\tmsDS-NC-RO-Replica-Locations=%s" % k
1359 return text
1362 class Site(object):
1363 """An individual site object discovered thru the configuration
1364 naming context. Contains all DSAs that exist within the site
1366 def __init__(self, site_dnstr, unix_now):
1367 self.site_dnstr = site_dnstr
1368 self.site_guid = None
1369 self.site_options = 0
1370 self.site_topo_generator = None
1371 self.site_topo_failover = 0 # appears to be in minutes
1372 self.dsa_table = {}
1373 self.rw_dsa_table = {}
1374 self.unix_now = unix_now
1376 def load_site(self, samdb):
1377 """Loads the NTDS Site Settions options attribute for the site
1378 as well as querying and loading all DSAs that appear within
1379 the site.
1381 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1382 attrs = ["options",
1383 "interSiteTopologyFailover",
1384 "interSiteTopologyGenerator"]
1385 try:
1386 res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE,
1387 attrs=attrs)
1388 self_res = samdb.search(base=self.site_dnstr, scope=ldb.SCOPE_BASE,
1389 attrs=['objectGUID'])
1390 except ldb.LdbError, (enum, estr):
1391 raise Exception("Unable to find site settings for (%s) - (%s)" %
1392 (ssdn, estr))
1394 msg = res[0]
1395 if "options" in msg:
1396 self.site_options = int(msg["options"][0])
1398 if "interSiteTopologyGenerator" in msg:
1399 self.site_topo_generator = \
1400 str(msg["interSiteTopologyGenerator"][0])
1402 if "interSiteTopologyFailover" in msg:
1403 self.site_topo_failover = int(msg["interSiteTopologyFailover"][0])
1405 msg = self_res[0]
1406 if "objectGUID" in msg:
1407 self.site_guid = misc.GUID(samdb.schema_format_value("objectGUID",
1408 msg["objectGUID"][0]))
1410 self.load_all_dsa(samdb)
1412 def load_all_dsa(self, samdb):
1413 """Discover all nTDSDSA thru the sites entry and
1414 instantiate and load the DSAs. Each dsa is inserted
1415 into the dsa_table by dn string.
1417 try:
1418 res = samdb.search(self.site_dnstr,
1419 scope=ldb.SCOPE_SUBTREE,
1420 expression="(objectClass=nTDSDSA)")
1421 except ldb.LdbError, (enum, estr):
1422 raise Exception("Unable to find nTDSDSAs - (%s)" % estr)
1424 for msg in res:
1425 dnstr = str(msg.dn)
1427 # already loaded
1428 if dnstr in self.dsa_table:
1429 continue
1431 dsa = DirectoryServiceAgent(dnstr)
1433 dsa.load_dsa(samdb)
1435 # Assign this dsa to my dsa table
1436 # and index by dsa dn
1437 self.dsa_table[dnstr] = dsa
1438 if not dsa.is_ro():
1439 self.rw_dsa_table[dnstr] = dsa
1441 def get_dsa_by_guidstr(self, guidstr): # XXX unused
1442 for dsa in self.dsa_table.values():
1443 if str(dsa.dsa_guid) == guidstr:
1444 return dsa
1445 return None
1447 def get_dsa(self, dnstr):
1448 """Return a previously loaded DSA object by consulting
1449 the sites dsa_table for the provided DSA dn string
1451 :return: None if DSA doesn't exist
1453 return self.dsa_table.get(dnstr)
1455 def select_istg(self, samdb, mydsa, ro):
1456 """Determine if my DC should be an intersite topology
1457 generator. If my DC is the istg and is both a writeable
1458 DC and the database is opened in write mode then we perform
1459 an originating update to set the interSiteTopologyGenerator
1460 attribute in the NTDS Site Settings object. An RODC always
1461 acts as an ISTG for itself.
1463 # The KCC on an RODC always acts as an ISTG for itself
1464 if mydsa.dsa_is_ro:
1465 mydsa.dsa_is_istg = True
1466 self.site_topo_generator = mydsa.dsa_dnstr
1467 return True
1469 c_rep = get_dsa_config_rep(mydsa)
1471 # Load repsFrom and replUpToDateVector if not already loaded
1472 # so we can get the current state of the config replica and
1473 # whether we are getting updates from the istg
1474 c_rep.load_repsFrom(samdb)
1476 c_rep.load_replUpToDateVector(samdb)
1478 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1479 # First, the KCC on a writable DC determines whether it acts
1480 # as an ISTG for its site
1482 # Let s be the object such that s!lDAPDisplayName = nTDSDSA
1483 # and classSchema in s!objectClass.
1485 # Let D be the sequence of objects o in the site of the local
1486 # DC such that o!objectCategory = s. D is sorted in ascending
1487 # order by objectGUID.
1489 # Which is a fancy way of saying "sort all the nTDSDSA objects
1490 # in the site by guid in ascending order". Place sorted list
1491 # in D_sort[]
1492 D_sort = sorted(self.rw_dsa_table.values(), cmp=sort_dsa_by_guid)
1494 # double word number of 100 nanosecond intervals since 1600s
1495 ntnow = unix2nttime(self.unix_now)
1497 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1498 # if o!interSiteTopologyFailover is 0 or has no value.
1500 # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1501 # so it appears we have to turn f into the same interval
1503 # interSiteTopologyFailover (if set) appears to be in minutes
1504 # so we'll need to convert to senconds and then 100 nanosecond
1505 # intervals
1506 # XXX [MS-ADTS] 6.2.2.3.1 says it is seconds, not minutes.
1508 # 10,000,000 is number of 100 nanosecond intervals in a second
1509 if self.site_topo_failover == 0:
1510 f = 2 * 60 * 60 * 10000000
1511 else:
1512 f = self.site_topo_failover * 60 * 10000000
1514 # Let o be the site settings object for the site of the local
1515 # DC, or NULL if no such o exists.
1516 d_dsa = self.dsa_table.get(self.site_topo_generator)
1518 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1519 # If o != NULL and o!interSiteTopologyGenerator is not the
1520 # nTDSDSA object for the local DC and
1521 # o!interSiteTopologyGenerator is an element dj of sequence D:
1523 if d_dsa is not None and d_dsa is not mydsa:
1524 # From MS-ADTS 6.2.2.3.1 ISTG Selection:
1525 # Let c be the cursor in the replUpToDateVector variable
1526 # associated with the NC replica of the config NC such
1527 # that c.uuidDsa = dj!invocationId. If no such c exists
1528 # (No evidence of replication from current ITSG):
1529 # Let i = j.
1530 # Let t = 0.
1532 # Else if the current time < c.timeLastSyncSuccess - f
1533 # (Evidence of time sync problem on current ISTG):
1534 # Let i = 0.
1535 # Let t = 0.
1537 # Else (Evidence of replication from current ITSG):
1538 # Let i = j.
1539 # Let t = c.timeLastSyncSuccess.
1541 # last_success appears to be a double word containing
1542 # number of 100 nanosecond intervals since the 1600s
1543 j_idx = D_sort.index(d_dsa)
1545 found = False
1546 for cursor in c_rep.rep_replUpToDateVector_cursors:
1547 if d_dsa.dsa_ivid == cursor.source_dsa_invocation_id:
1548 found = True
1549 break
1551 if not found:
1552 i_idx = j_idx
1553 t_time = 0
1555 #XXX doc says current time < c.timeLastSyncSuccess - f
1556 # which is true only if f is negative or clocks are wrong.
1557 # f is not negative in the default case (2 hours).
1558 elif ntnow - cursor.last_sync_success > f:
1559 i_idx = 0
1560 t_time = 0
1561 else:
1562 i_idx = j_idx
1563 t_time = cursor.last_sync_success
1565 # Otherwise (Nominate local DC as ISTG):
1566 # Let i be the integer such that di is the nTDSDSA
1567 # object for the local DC.
1568 # Let t = the current time.
1569 else:
1570 i_idx = D_sort.index(mydsa)
1571 t_time = ntnow
1573 # Compute a function that maintains the current ISTG if
1574 # it is alive, cycles through other candidates if not.
1576 # Let k be the integer (i + ((current time - t) /
1577 # o!interSiteTopologyFailover)) MOD |D|.
1579 # Note: We don't want to divide by zero here so they must
1580 # have meant "f" instead of "o!interSiteTopologyFailover"
1581 k_idx = (i_idx + ((ntnow - t_time) / f)) % len(D_sort)
1583 # The local writable DC acts as an ISTG for its site if and
1584 # only if dk is the nTDSDSA object for the local DC. If the
1585 # local DC does not act as an ISTG, the KCC skips the
1586 # remainder of this task.
1587 d_dsa = D_sort[k_idx]
1588 d_dsa.dsa_is_istg = True
1590 # Update if we are the ISTG, otherwise return
1591 if d_dsa is not mydsa:
1592 return False
1594 # Nothing to do
1595 if self.site_topo_generator == mydsa.dsa_dnstr:
1596 return True
1598 self.site_topo_generator = mydsa.dsa_dnstr
1600 # If readonly database then do not perform a
1601 # persistent update
1602 if ro:
1603 return True
1605 # Perform update to the samdb
1606 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1608 m = ldb.Message()
1609 m.dn = ldb.Dn(samdb, ssdn)
1611 m["interSiteTopologyGenerator"] = \
1612 ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
1613 "interSiteTopologyGenerator")
1614 try:
1615 samdb.modify(m)
1617 except ldb.LdbError, estr:
1618 raise Exception(
1619 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1620 (ssdn, estr))
1621 return True
1623 def is_intrasite_topology_disabled(self):
1624 '''Returns True if intra-site topology is disabled for site'''
1625 return (self.site_options &
1626 dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0
1628 def is_intersite_topology_disabled(self):
1629 '''Returns True if inter-site topology is disabled for site'''
1630 return ((self.site_options &
1631 dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED)
1632 != 0)
1634 def is_random_bridgehead_disabled(self):
1635 '''Returns True if selection of random bridgehead is disabled'''
1636 return (self.site_options &
1637 dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0
1639 def is_detect_stale_disabled(self):
1640 '''Returns True if detect stale is disabled for site'''
1641 return (self.site_options &
1642 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0
1644 def is_cleanup_ntdsconn_disabled(self):
1645 '''Returns True if NTDS Connection cleanup is disabled for site'''
1646 return (self.site_options &
1647 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0
1649 def same_site(self, dsa):
1650 '''Return True if dsa is in this site'''
1651 if self.get_dsa(dsa.dsa_dnstr):
1652 return True
1653 return False
1655 def __str__(self):
1656 '''Debug dump string output of class'''
1657 text = "%s:" % self.__class__.__name__
1658 text = text + "\n\tdn=%s" % self.site_dnstr
1659 text = text + "\n\toptions=0x%X" % self.site_options
1660 text = text + "\n\ttopo_generator=%s" % self.site_topo_generator
1661 text = text + "\n\ttopo_failover=%d" % self.site_topo_failover
1662 for key, dsa in self.dsa_table.items():
1663 text = text + "\n%s" % dsa
1664 return text
1667 class GraphNode(object):
1668 """A graph node describing a set of edges that should be directed to it.
1670 Each edge is a connection for a particular naming context replica directed
1671 from another node in the forest to this node.
1674 def __init__(self, dsa_dnstr, max_node_edges):
1675 """Instantiate the graph node according to a DSA dn string
1677 :param max_node_edges: maximum number of edges that should ever
1678 be directed to the node
1680 self.max_edges = max_node_edges
1681 self.dsa_dnstr = dsa_dnstr
1682 self.edge_from = []
1684 def __str__(self):
1685 text = "%s:" % self.__class__.__name__
1686 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
1687 text = text + "\n\tmax_edges=%d" % self.max_edges
1689 for i, edge in enumerate(self.edge_from):
1690 text = text + "\n\tedge_from[%d]=%s" % (i, edge)
1691 return text
1693 def add_edge_from(self, from_dsa_dnstr):
1694 """Add an edge from the dsa to our graph nodes edge from list
1696 :param from_dsa_dnstr: the dsa that the edge emanates from
1698 assert from_dsa_dnstr is not None
1700 # No edges from myself to myself
1701 if from_dsa_dnstr == self.dsa_dnstr:
1702 return False
1703 # Only one edge from a particular node
1704 if from_dsa_dnstr in self.edge_from:
1705 return False
1706 # Not too many edges
1707 if len(self.edge_from) >= self.max_edges:
1708 return False
1709 self.edge_from.append(from_dsa_dnstr)
1710 return True
1712 def add_edges_from_connections(self, dsa):
1713 """For each nTDSConnection object associated with a particular
1714 DSA, we test if it implies an edge to this graph node (i.e.
1715 the "fromServer" attribute). If it does then we add an
1716 edge from the server unless we are over the max edges for this
1717 graph node
1719 :param dsa: dsa with a dnstr equivalent to his graph node
1721 for connect in dsa.connect_table.values():
1722 self.add_edge_from(connect.from_dnstr)
1724 def add_connections_from_edges(self, dsa):
1725 """For each edge directed to this graph node, ensure there
1726 is a corresponding nTDSConnection object in the dsa.
1728 for edge_dnstr in self.edge_from:
1729 connections = dsa.get_connection_by_from_dnstr(edge_dnstr)
1731 # For each edge directed to the NC replica that
1732 # "should be present" on the local DC, the KCC determines
1733 # whether an object c exists such that:
1735 # c is a child of the DC's nTDSDSA object.
1736 # c.objectCategory = nTDSConnection
1738 # Given the NC replica ri from which the edge is directed,
1739 # c.fromServer is the dsname of the nTDSDSA object of
1740 # the DC on which ri "is present".
1742 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1744 found_valid = False
1745 for connect in connections:
1746 if connect.is_rodc_topology():
1747 continue
1748 found_valid = True
1750 if found_valid:
1751 continue
1753 # if no such object exists then the KCC adds an object
1754 # c with the following attributes
1756 # Generate a new dnstr for this nTDSConnection
1757 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1758 flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1759 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1761 dsa.new_connection(opt, flags, None, edge_dnstr, None)
1763 def has_sufficient_edges(self):
1764 '''Return True if we have met the maximum "from edges" criteria'''
1765 if len(self.edge_from) >= self.max_edges:
1766 return True
1767 return False
1770 class Transport(object):
1771 """Class defines a Inter-site transport found under Sites
1774 def __init__(self, dnstr):
1775 self.dnstr = dnstr
1776 self.options = 0
1777 self.guid = None
1778 self.name = None
1779 self.address_attr = None
1780 self.bridgehead_list = []
1782 def __str__(self):
1783 '''Debug dump string output of Transport object'''
1785 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
1786 text = text + "\n\tguid=%s" % str(self.guid)
1787 text = text + "\n\toptions=%d" % self.options
1788 text = text + "\n\taddress_attr=%s" % self.address_attr
1789 text = text + "\n\tname=%s" % self.name
1790 for dnstr in self.bridgehead_list:
1791 text = text + "\n\tbridgehead_list=%s" % dnstr
1793 return text
1795 def load_transport(self, samdb):
1796 """Given a Transport object with an prior initialization
1797 for the object's DN, search for the DN and load attributes
1798 from the samdb.
1800 attrs = ["objectGUID",
1801 "options",
1802 "name",
1803 "bridgeheadServerListBL",
1804 "transportAddressAttribute"]
1805 try:
1806 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
1807 attrs=attrs)
1809 except ldb.LdbError, (enum, estr):
1810 raise Exception("Unable to find Transport for (%s) - (%s)" %
1811 (self.dnstr, estr))
1813 msg = res[0]
1814 self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
1815 msg["objectGUID"][0]))
1817 if "options" in msg:
1818 self.options = int(msg["options"][0])
1820 if "transportAddressAttribute" in msg:
1821 self.address_attr = str(msg["transportAddressAttribute"][0])
1823 if "name" in msg:
1824 self.name = str(msg["name"][0])
1826 if "bridgeheadServerListBL" in msg:
1827 for value in msg["bridgeheadServerListBL"]:
1828 dsdn = dsdb_Dn(samdb, value)
1829 dnstr = str(dsdn.dn)
1830 if dnstr not in self.bridgehead_list:
1831 self.bridgehead_list.append(dnstr)
1834 class RepsFromTo(object):
1835 """Class encapsulation of the NDR repsFromToBlob.
1837 Removes the necessity of external code having to
1838 understand about other_info or manipulation of
1839 update flags.
1841 def __init__(self, nc_dnstr=None, ndr_blob=None):
1843 self.__dict__['to_be_deleted'] = False
1844 self.__dict__['nc_dnstr'] = nc_dnstr
1845 self.__dict__['update_flags'] = 0x0
1846 # XXX the following sounds dubious and/or better solved
1847 # elsewhere, but lets leave it for now. In particular, there
1848 # seems to be no reason for all the non-ndr generated
1849 # attributes to be handled in the round about way (e.g.
1850 # self.__dict__['to_be_deleted'] = False above). On the other
1851 # hand, it all seems to work. Hooray! Hands off!.
1853 # WARNING:
1855 # There is a very subtle bug here with python
1856 # and our NDR code. If you assign directly to
1857 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1858 # then a proper python GC reference count is not
1859 # maintained.
1861 # To work around this we maintain an internal
1862 # reference to "dns_name(x)" and "other_info" elements
1863 # of repsFromToBlob. This internal reference
1864 # is hidden within this class but it is why you
1865 # see statements like this below:
1867 # self.__dict__['ndr_blob'].ctr.other_info = \
1868 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1870 # That would appear to be a redundant assignment but
1871 # it is necessary to hold a proper python GC reference
1872 # count.
1873 if ndr_blob is None:
1874 self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
1875 self.__dict__['ndr_blob'].version = 0x1
1876 self.__dict__['dns_name1'] = None
1877 self.__dict__['dns_name2'] = None
1879 self.__dict__['ndr_blob'].ctr.other_info = \
1880 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1882 else:
1883 self.__dict__['ndr_blob'] = ndr_blob
1884 self.__dict__['other_info'] = ndr_blob.ctr.other_info
1886 if ndr_blob.version == 0x1:
1887 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
1888 self.__dict__['dns_name2'] = None
1889 else:
1890 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
1891 self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
1893 def __str__(self):
1894 '''Debug dump string output of class'''
1896 text = "%s:" % self.__class__.__name__
1897 text += "\n\tdnstr=%s" % self.nc_dnstr
1898 text += "\n\tupdate_flags=0x%X" % self.update_flags
1899 text += "\n\tversion=%d" % self.version
1900 text += "\n\tsource_dsa_obj_guid=%s" % self.source_dsa_obj_guid
1901 text += ("\n\tsource_dsa_invocation_id=%s" %
1902 self.source_dsa_invocation_id)
1903 text += "\n\ttransport_guid=%s" % self.transport_guid
1904 text += "\n\treplica_flags=0x%X" % self.replica_flags
1905 text += ("\n\tconsecutive_sync_failures=%d" %
1906 self.consecutive_sync_failures)
1907 text += "\n\tlast_success=%s" % self.last_success
1908 text += "\n\tlast_attempt=%s" % self.last_attempt
1909 text += "\n\tdns_name1=%s" % self.dns_name1
1910 text += "\n\tdns_name2=%s" % self.dns_name2
1911 text += "\n\tschedule[ "
1912 for slot in self.schedule:
1913 text += "0x%X " % slot
1914 text += "]"
1916 return text
1918 def __setattr__(self, item, value):
1920 if item in ['schedule', 'replica_flags', 'transport_guid',
1921 'source_dsa_obj_guid', 'source_dsa_invocation_id',
1922 'consecutive_sync_failures', 'last_success',
1923 'last_attempt']:
1925 if item in ['replica_flags']:
1926 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
1927 elif item in ['schedule']:
1928 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
1930 setattr(self.__dict__['ndr_blob'].ctr, item, value)
1932 elif item in ['dns_name1']:
1933 self.__dict__['dns_name1'] = value
1935 if self.__dict__['ndr_blob'].version == 0x1:
1936 self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
1937 self.__dict__['dns_name1']
1938 else:
1939 self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
1940 self.__dict__['dns_name1']
1942 elif item in ['dns_name2']:
1943 self.__dict__['dns_name2'] = value
1945 if self.__dict__['ndr_blob'].version == 0x1:
1946 raise AttributeError(item)
1947 else:
1948 self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
1949 self.__dict__['dns_name2']
1951 elif item in ['nc_dnstr']:
1952 self.__dict__['nc_dnstr'] = value
1954 elif item in ['to_be_deleted']:
1955 self.__dict__['to_be_deleted'] = value
1957 elif item in ['version']:
1958 raise AttributeError("Attempt to set readonly attribute %s" % item)
1959 else:
1960 raise AttributeError("Unknown attribute %s" % item)
1962 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
1964 def __getattr__(self, item):
1965 """Overload of RepsFromTo attribute retrieval.
1967 Allows external code to ignore substructures within the blob
1969 if item in ['schedule', 'replica_flags', 'transport_guid',
1970 'source_dsa_obj_guid', 'source_dsa_invocation_id',
1971 'consecutive_sync_failures', 'last_success',
1972 'last_attempt']:
1973 return getattr(self.__dict__['ndr_blob'].ctr, item)
1975 elif item in ['version']:
1976 return self.__dict__['ndr_blob'].version
1978 elif item in ['dns_name1']:
1979 if self.__dict__['ndr_blob'].version == 0x1:
1980 return self.__dict__['ndr_blob'].ctr.other_info.dns_name
1981 else:
1982 return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
1984 elif item in ['dns_name2']:
1985 if self.__dict__['ndr_blob'].version == 0x1:
1986 raise AttributeError(item)
1987 else:
1988 return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
1990 elif item in ['to_be_deleted']:
1991 return self.__dict__['to_be_deleted']
1993 elif item in ['nc_dnstr']:
1994 return self.__dict__['nc_dnstr']
1996 elif item in ['update_flags']:
1997 return self.__dict__['update_flags']
1999 raise AttributeError("Unknown attribute %s" % item)
2001 def is_modified(self):
2002 return (self.update_flags != 0x0)
2004 def set_unmodified(self):
2005 self.__dict__['update_flags'] = 0x0
2008 class SiteLink(object):
2009 """Class defines a site link found under sites
2012 def __init__(self, dnstr):
2013 self.dnstr = dnstr
2014 self.options = 0
2015 self.system_flags = 0
2016 self.cost = 0
2017 self.schedule = None
2018 self.interval = None
2019 self.site_list = []
2021 def __str__(self):
2022 '''Debug dump string output of Transport object'''
2024 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
2025 text = text + "\n\toptions=%d" % self.options
2026 text = text + "\n\tsystem_flags=%d" % self.system_flags
2027 text = text + "\n\tcost=%d" % self.cost
2028 text = text + "\n\tinterval=%s" % self.interval
2030 if self.schedule is not None:
2031 text += "\n\tschedule.size=%s" % self.schedule.size
2032 text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
2033 text += ("\n\tschedule.numberOfSchedules=%s" %
2034 self.schedule.numberOfSchedules)
2036 for i, header in enumerate(self.schedule.headerArray):
2037 text += ("\n\tschedule.headerArray[%d].type=%d" %
2038 (i, header.type))
2039 text += ("\n\tschedule.headerArray[%d].offset=%d" %
2040 (i, header.offset))
2041 text = text + "\n\tschedule.dataArray[%d].slots[ " % i
2042 for slot in self.schedule.dataArray[i].slots:
2043 text = text + "0x%X " % slot
2044 text = text + "]"
2046 for dnstr in self.site_list:
2047 text = text + "\n\tsite_list=%s" % dnstr
2048 return text
2050 def load_sitelink(self, samdb):
2051 """Given a siteLink object with an prior initialization
2052 for the object's DN, search for the DN and load attributes
2053 from the samdb.
2055 attrs = ["options",
2056 "systemFlags",
2057 "cost",
2058 "schedule",
2059 "replInterval",
2060 "siteList"]
2061 try:
2062 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
2063 attrs=attrs, controls=['extended_dn:0'])
2065 except ldb.LdbError, (enum, estr):
2066 raise Exception("Unable to find SiteLink for (%s) - (%s)" %
2067 (self.dnstr, estr))
2069 msg = res[0]
2071 if "options" in msg:
2072 self.options = int(msg["options"][0])
2074 if "systemFlags" in msg:
2075 self.system_flags = int(msg["systemFlags"][0])
2077 if "cost" in msg:
2078 self.cost = int(msg["cost"][0])
2080 if "replInterval" in msg:
2081 self.interval = int(msg["replInterval"][0])
2083 if "siteList" in msg:
2084 for value in msg["siteList"]:
2085 dsdn = dsdb_Dn(samdb, value)
2086 guid = misc.GUID(dsdn.dn.get_extended_component('GUID'))
2087 if guid not in self.site_list:
2088 self.site_list.append(guid)
2090 if "schedule" in msg:
2091 self.schedule = ndr_unpack(drsblobs.schedule, value)
2092 else:
2093 self.schedule = new_connection_schedule()
2096 class KCCFailedObject(object):
2097 def __init__(self, uuid, failure_count, time_first_failure,
2098 last_result, dns_name):
2099 self.uuid = uuid
2100 self.failure_count = failure_count
2101 self.time_first_failure = time_first_failure
2102 self.last_result = last_result
2103 self.dns_name = dns_name
2106 class VertexColor(object):
2107 (red, black, white, unknown) = range(0, 4)
2110 class Vertex(object):
2111 """Class encapsulation of a Site Vertex in the
2112 intersite topology replication algorithm
2114 def __init__(self, site, part):
2115 self.site = site
2116 self.part = part
2117 self.color = VertexColor.unknown
2118 self.edges = []
2119 self.accept_red_red = []
2120 self.accept_black = []
2121 self.repl_info = ReplInfo()
2122 self.root = self
2123 self.guid = None
2124 self.component_id = self
2125 self.demoted = False
2126 self.options = 0
2127 self.interval = 0
2129 def color_vertex(self):
2130 """Color each vertex to indicate which kind of NC
2131 replica it contains
2133 # IF s contains one or more DCs with full replicas of the
2134 # NC cr!nCName
2135 # SET v.Color to COLOR.RED
2136 # ELSEIF s contains one or more partial replicas of the NC
2137 # SET v.Color to COLOR.BLACK
2138 #ELSE
2139 # SET v.Color to COLOR.WHITE
2141 # set to minimum (no replica)
2142 self.color = VertexColor.white
2144 for dnstr, dsa in self.site.dsa_table.items():
2145 rep = dsa.get_current_replica(self.part.nc_dnstr)
2146 if rep is None:
2147 continue
2149 # We have a full replica which is the largest
2150 # value so exit
2151 if not rep.is_partial():
2152 self.color = VertexColor.red
2153 break
2154 else:
2155 self.color = VertexColor.black
2157 def is_red(self):
2158 assert(self.color != VertexColor.unknown)
2159 return (self.color == VertexColor.red)
2161 def is_black(self):
2162 assert(self.color != VertexColor.unknown)
2163 return (self.color == VertexColor.black)
2165 def is_white(self):
2166 assert(self.color != VertexColor.unknown)
2167 return (self.color == VertexColor.white)
2170 class IntersiteGraph(object):
2171 """Graph for representing the intersite"""
2172 def __init__(self):
2173 self.vertices = set()
2174 self.edges = set()
2175 self.edge_set = set()
2176 # All vertices that are endpoints of edges
2177 self.connected_vertices = None
2180 class MultiEdgeSet(object):
2181 """Defines a multi edge set"""
2182 def __init__(self):
2183 self.guid = 0 # objectGuid siteLinkBridge
2184 self.edges = []
2187 class MultiEdge(object):
2188 def __init__(self):
2189 self.site_link = None # object siteLink
2190 self.vertices = []
2191 self.con_type = None # interSiteTransport GUID
2192 self.repl_info = ReplInfo()
2193 self.directed = True
2196 class ReplInfo(object):
2197 def __init__(self):
2198 self.cost = 0
2199 self.interval = 0
2200 self.options = 0
2201 self.schedule = None
2204 class InternalEdge(object):
2205 def __init__(self, v1, v2, redred, repl, eType, site_link):
2206 self.v1 = v1
2207 self.v2 = v2
2208 self.red_red = redred
2209 self.repl_info = repl
2210 self.e_type = eType
2211 self.site_link = site_link
2213 def __eq__(self, other):
2214 return not self < other and not other < self
2216 def __ne__(self, other):
2217 return self < other or other < self
2219 def __gt__(self, other):
2220 return other < self
2222 def __ge__(self, other):
2223 return not self < other
2225 def __le__(self, other):
2226 return not other < self
2228 # TODO compare options and interval
2229 def __lt__(self, other):
2230 if self.red_red != other.red_red:
2231 return self.red_red
2233 if self.repl_info.cost != other.repl_info.cost:
2234 return self.repl_info.cost < other.repl_info.cost
2236 self_time = total_schedule(self.repl_info.schedule)
2237 other_time = total_schedule(other.repl_info.schedule)
2238 if self_time != other_time:
2239 return self_time > other_time
2241 #XXX guid comparison using ndr_pack
2242 if self.v1.guid != other.v1.guid:
2243 return self.v1.ndrpacked_guid < other.v1.ndrpacked_guid
2245 if self.v2.guid != other.v2.guid:
2246 return self.v2.ndrpacked_guid < other.v2.ndrpacked_guid
2248 return self.e_type < other.e_type
2251 ##################################################
2252 # Global Functions and Variables
2253 ##################################################
2254 MAX_DWORD = 2 ** 32 - 1
2257 def get_dsa_config_rep(dsa):
2258 # Find configuration NC replica for the DSA
2259 for c_rep in dsa.current_rep_table.values():
2260 if c_rep.is_config():
2261 return c_rep
2263 raise KCCError("Unable to find config NC replica for (%s)" %
2264 dsa.dsa_dnstr)
2267 def sort_dsa_by_guid(dsa1, dsa2):
2268 "use ndr_pack for GUID comparison, as appears correct in some places"""
2269 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
2272 def total_schedule(schedule):
2273 if schedule is None:
2274 return 84 * 8 # 84 bytes = 84 * 8 bits
2276 total = 0
2277 for byte in schedule:
2278 while byte != 0:
2279 total += byte & 1
2280 byte >>= 1
2281 return total
2284 # Returns true if schedule intersect
2285 def combine_repl_info(info_a, info_b, info_c):
2286 info_c.interval = max(info_a.interval, info_b.interval)
2287 info_c.options = info_a.options & info_b.options
2289 if info_a.schedule is None:
2290 info_a.schedule = [0xFF] * 84
2291 if info_b.schedule is None:
2292 info_b.schedule = [0xFF] * 84
2294 new_info = [0] * 84
2295 i = 0
2296 count = 0
2297 while i < 84:
2298 # Note that this operation is actually bitwise
2299 new_info = info_a.schedule[i] & info_b.schedule[i]
2300 if new_info != 0:
2301 count += 1
2302 i += 1
2304 if count == 0:
2305 return False
2307 info_c.schedule = new_info
2309 # Truncate to MAX_DWORD
2310 info_c.cost = info_a.cost + info_b.cost
2311 if info_c.cost > MAX_DWORD:
2312 info_c.cost = MAX_DWORD
2314 return True
2317 def convert_schedule_to_repltimes(schedule):
2318 """Convert NTDS Connection schedule to replTime schedule.
2320 Schedule defined in MS-ADTS 6.1.4.5.2
2321 ReplTimes defined in MS-DRSR 5.164.
2323 "Schedule" has 168 bytes but only the lower nibble of each is
2324 significant. There is one byte per hour. Bit 3 (0x08) represents
2325 the first 15 minutes of the hour and bit 0 (0x01) represents the
2326 last 15 minutes. The first byte presumably covers 12am - 1am
2327 Sunday, though the spec doesn't define the start of a week.
2329 "ReplTimes" has 84 bytes which are the 168 lower nibbles of
2330 "Schedule" packed together. Thus each byte covers 2 hours. Bits 7
2331 (i.e. 0x80) is the first 15 minutes and bit 0 is the last. The
2332 first byte covers Sunday 12am - 2am (per spec).
2334 Here we pack two elements of the NTDS Connection schedule slots
2335 into one element of the replTimes list.
2337 If no schedule appears in NTDS Connection then a default of 0x11
2338 is set in each replTimes slot as per behaviour noted in a Windows
2339 DC. That default would cause replication within the last 15
2340 minutes of each hour.
2342 if schedule is None or schedule.dataArray[0] is None:
2343 return [0x11] * 84
2345 times = []
2346 data = schedule.dataArray[0].slots
2348 for i in range(84):
2349 times.append((data[i * 2] & 0xF) << 4 | (data[i * 2 + 1] & 0xF))
2351 return times
2354 def new_connection_schedule():
2355 """Create a default schedule for an NTDSConnection or Sitelink. This
2356 is packed differently from the repltimes schedule used elsewhere
2357 in KCC (where the 168 nibbles are packed into 84 bytes).
2359 # 168 byte instances of the 0x01 value. The low order 4 bits
2360 # of the byte equate to 15 minute intervals within a single hour.
2361 # There are 168 bytes because there are 168 hours in a full week
2362 # Effectively we are saying to perform replication at the end of
2363 # each hour of the week
2364 schedule = drsblobs.schedule()
2366 schedule.size = 188
2367 schedule.bandwidth = 0
2368 schedule.numberOfSchedules = 1
2370 header = drsblobs.scheduleHeader()
2371 header.type = 0
2372 header.offset = 20
2374 schedule.headerArray = [header]
2376 data = drsblobs.scheduleSlots()
2377 data.slots = [0x01] * 168
2379 schedule.dataArray = [data]
2380 return schedule