kcc: Allow RODC to be included in intersite topology
[Samba.git] / python / samba / kcc / kcc_utils.py
blobfa8659f0b853dc3b8f685a3531d4d0c33142bb7a
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
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, nt_now):
1367 self.site_dnstr = site_dnstr
1368 self.site_guid = None
1369 self.site_options = 0
1370 self.site_topo_generator = None
1371 self.site_topo_failover = 0 # appears to be in minutes
1372 self.dsa_table = {}
1373 self.rw_dsa_table = {}
1374 self.nt_now = nt_now
1376 def load_site(self, samdb):
1377 """Loads the NTDS Site Settions options attribute for the site
1378 as well as querying and loading all DSAs that appear within
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
1496 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1497 # if o!interSiteTopologyFailover is 0 or has no value.
1499 # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1500 # so it appears we have to turn f into the same interval
1502 # interSiteTopologyFailover (if set) appears to be in minutes
1503 # so we'll need to convert to senconds and then 100 nanosecond
1504 # intervals
1505 # XXX [MS-ADTS] 6.2.2.3.1 says it is seconds, not minutes.
1507 # 10,000,000 is number of 100 nanosecond intervals in a second
1508 if self.site_topo_failover == 0:
1509 f = 2 * 60 * 60 * 10000000
1510 else:
1511 f = self.site_topo_failover * 60 * 10000000
1513 # Let o be the site settings object for the site of the local
1514 # DC, or NULL if no such o exists.
1515 d_dsa = self.dsa_table.get(self.site_topo_generator)
1517 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1518 # If o != NULL and o!interSiteTopologyGenerator is not the
1519 # nTDSDSA object for the local DC and
1520 # o!interSiteTopologyGenerator is an element dj of sequence D:
1522 if d_dsa is not None and d_dsa is not mydsa:
1523 # From MS-ADTS 6.2.2.3.1 ISTG Selection:
1524 # Let c be the cursor in the replUpToDateVector variable
1525 # associated with the NC replica of the config NC such
1526 # that c.uuidDsa = dj!invocationId. If no such c exists
1527 # (No evidence of replication from current ITSG):
1528 # Let i = j.
1529 # Let t = 0.
1531 # Else if the current time < c.timeLastSyncSuccess - f
1532 # (Evidence of time sync problem on current ISTG):
1533 # Let i = 0.
1534 # Let t = 0.
1536 # Else (Evidence of replication from current ITSG):
1537 # Let i = j.
1538 # Let t = c.timeLastSyncSuccess.
1540 # last_success appears to be a double word containing
1541 # number of 100 nanosecond intervals since the 1600s
1542 j_idx = D_sort.index(d_dsa)
1544 found = False
1545 for cursor in c_rep.rep_replUpToDateVector_cursors:
1546 if d_dsa.dsa_ivid == cursor.source_dsa_invocation_id:
1547 found = True
1548 break
1550 if not found:
1551 i_idx = j_idx
1552 t_time = 0
1554 #XXX doc says current time < c.timeLastSyncSuccess - f
1555 # which is true only if f is negative or clocks are wrong.
1556 # f is not negative in the default case (2 hours).
1557 elif self.nt_now - cursor.last_sync_success > f:
1558 i_idx = 0
1559 t_time = 0
1560 else:
1561 i_idx = j_idx
1562 t_time = cursor.last_sync_success
1564 # Otherwise (Nominate local DC as ISTG):
1565 # Let i be the integer such that di is the nTDSDSA
1566 # object for the local DC.
1567 # Let t = the current time.
1568 else:
1569 i_idx = D_sort.index(mydsa)
1570 t_time = self.nt_now
1572 # Compute a function that maintains the current ISTG if
1573 # it is alive, cycles through other candidates if not.
1575 # Let k be the integer (i + ((current time - t) /
1576 # o!interSiteTopologyFailover)) MOD |D|.
1578 # Note: We don't want to divide by zero here so they must
1579 # have meant "f" instead of "o!interSiteTopologyFailover"
1580 k_idx = (i_idx + ((self.nt_now - t_time) / f)) % len(D_sort)
1582 # The local writable DC acts as an ISTG for its site if and
1583 # only if dk is the nTDSDSA object for the local DC. If the
1584 # local DC does not act as an ISTG, the KCC skips the
1585 # remainder of this task.
1586 d_dsa = D_sort[k_idx]
1587 d_dsa.dsa_is_istg = True
1589 # Update if we are the ISTG, otherwise return
1590 if d_dsa is not mydsa:
1591 return False
1593 # Nothing to do
1594 if self.site_topo_generator == mydsa.dsa_dnstr:
1595 return True
1597 self.site_topo_generator = mydsa.dsa_dnstr
1599 # If readonly database then do not perform a
1600 # persistent update
1601 if ro:
1602 return True
1604 # Perform update to the samdb
1605 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1607 m = ldb.Message()
1608 m.dn = ldb.Dn(samdb, ssdn)
1610 m["interSiteTopologyGenerator"] = \
1611 ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
1612 "interSiteTopologyGenerator")
1613 try:
1614 samdb.modify(m)
1616 except ldb.LdbError, estr:
1617 raise Exception(
1618 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1619 (ssdn, estr))
1620 return True
1622 def is_intrasite_topology_disabled(self):
1623 '''Returns True if intra-site topology is disabled for site'''
1624 return (self.site_options &
1625 dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0
1627 def is_intersite_topology_disabled(self):
1628 '''Returns True if inter-site topology is disabled for site'''
1629 return ((self.site_options &
1630 dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED)
1631 != 0)
1633 def is_random_bridgehead_disabled(self):
1634 '''Returns True if selection of random bridgehead is disabled'''
1635 return (self.site_options &
1636 dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0
1638 def is_detect_stale_disabled(self):
1639 '''Returns True if detect stale is disabled for site'''
1640 return (self.site_options &
1641 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0
1643 def is_cleanup_ntdsconn_disabled(self):
1644 '''Returns True if NTDS Connection cleanup is disabled for site'''
1645 return (self.site_options &
1646 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0
1648 def same_site(self, dsa):
1649 '''Return True if dsa is in this site'''
1650 if self.get_dsa(dsa.dsa_dnstr):
1651 return True
1652 return False
1654 def is_rodc_site(self):
1655 if len(self.dsa_table) > 0 and len(self.rw_dsa_table) == 0:
1656 return True
1657 return False
1659 def __str__(self):
1660 '''Debug dump string output of class'''
1661 text = "%s:" % self.__class__.__name__
1662 text = text + "\n\tdn=%s" % self.site_dnstr
1663 text = text + "\n\toptions=0x%X" % self.site_options
1664 text = text + "\n\ttopo_generator=%s" % self.site_topo_generator
1665 text = text + "\n\ttopo_failover=%d" % self.site_topo_failover
1666 for key, dsa in self.dsa_table.items():
1667 text = text + "\n%s" % dsa
1668 return text
1671 class GraphNode(object):
1672 """A graph node describing a set of edges that should be directed to it.
1674 Each edge is a connection for a particular naming context replica directed
1675 from another node in the forest to this node.
1678 def __init__(self, dsa_dnstr, max_node_edges):
1679 """Instantiate the graph node according to a DSA dn string
1681 :param max_node_edges: maximum number of edges that should ever
1682 be directed to the node
1684 self.max_edges = max_node_edges
1685 self.dsa_dnstr = dsa_dnstr
1686 self.edge_from = []
1688 def __str__(self):
1689 text = "%s:" % self.__class__.__name__
1690 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
1691 text = text + "\n\tmax_edges=%d" % self.max_edges
1693 for i, edge in enumerate(self.edge_from):
1694 text = text + "\n\tedge_from[%d]=%s" % (i, edge)
1695 return text
1697 def add_edge_from(self, from_dsa_dnstr):
1698 """Add an edge from the dsa to our graph nodes edge from list
1700 :param from_dsa_dnstr: the dsa that the edge emanates from
1702 assert from_dsa_dnstr is not None
1704 # No edges from myself to myself
1705 if from_dsa_dnstr == self.dsa_dnstr:
1706 return False
1707 # Only one edge from a particular node
1708 if from_dsa_dnstr in self.edge_from:
1709 return False
1710 # Not too many edges
1711 if len(self.edge_from) >= self.max_edges:
1712 return False
1713 self.edge_from.append(from_dsa_dnstr)
1714 return True
1716 def add_edges_from_connections(self, dsa):
1717 """For each nTDSConnection object associated with a particular
1718 DSA, we test if it implies an edge to this graph node (i.e.
1719 the "fromServer" attribute). If it does then we add an
1720 edge from the server unless we are over the max edges for this
1721 graph node
1723 :param dsa: dsa with a dnstr equivalent to his graph node
1725 for connect in dsa.connect_table.values():
1726 self.add_edge_from(connect.from_dnstr)
1728 def add_connections_from_edges(self, dsa):
1729 """For each edge directed to this graph node, ensure there
1730 is a corresponding nTDSConnection object in the dsa.
1732 for edge_dnstr in self.edge_from:
1733 connections = dsa.get_connection_by_from_dnstr(edge_dnstr)
1735 # For each edge directed to the NC replica that
1736 # "should be present" on the local DC, the KCC determines
1737 # whether an object c exists such that:
1739 # c is a child of the DC's nTDSDSA object.
1740 # c.objectCategory = nTDSConnection
1742 # Given the NC replica ri from which the edge is directed,
1743 # c.fromServer is the dsname of the nTDSDSA object of
1744 # the DC on which ri "is present".
1746 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1748 found_valid = False
1749 for connect in connections:
1750 if connect.is_rodc_topology():
1751 continue
1752 found_valid = True
1754 if found_valid:
1755 continue
1757 # if no such object exists then the KCC adds an object
1758 # c with the following attributes
1760 # Generate a new dnstr for this nTDSConnection
1761 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1762 flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1763 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1765 dsa.new_connection(opt, flags, None, edge_dnstr, None)
1767 def has_sufficient_edges(self):
1768 '''Return True if we have met the maximum "from edges" criteria'''
1769 if len(self.edge_from) >= self.max_edges:
1770 return True
1771 return False
1774 class Transport(object):
1775 """Class defines a Inter-site transport found under Sites
1778 def __init__(self, dnstr):
1779 self.dnstr = dnstr
1780 self.options = 0
1781 self.guid = None
1782 self.name = None
1783 self.address_attr = None
1784 self.bridgehead_list = []
1786 def __str__(self):
1787 '''Debug dump string output of Transport object'''
1789 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
1790 text = text + "\n\tguid=%s" % str(self.guid)
1791 text = text + "\n\toptions=%d" % self.options
1792 text = text + "\n\taddress_attr=%s" % self.address_attr
1793 text = text + "\n\tname=%s" % self.name
1794 for dnstr in self.bridgehead_list:
1795 text = text + "\n\tbridgehead_list=%s" % dnstr
1797 return text
1799 def load_transport(self, samdb):
1800 """Given a Transport object with an prior initialization
1801 for the object's DN, search for the DN and load attributes
1802 from the samdb.
1804 attrs = ["objectGUID",
1805 "options",
1806 "name",
1807 "bridgeheadServerListBL",
1808 "transportAddressAttribute"]
1809 try:
1810 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
1811 attrs=attrs)
1813 except ldb.LdbError, (enum, estr):
1814 raise Exception("Unable to find Transport for (%s) - (%s)" %
1815 (self.dnstr, estr))
1817 msg = res[0]
1818 self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
1819 msg["objectGUID"][0]))
1821 if "options" in msg:
1822 self.options = int(msg["options"][0])
1824 if "transportAddressAttribute" in msg:
1825 self.address_attr = str(msg["transportAddressAttribute"][0])
1827 if "name" in msg:
1828 self.name = str(msg["name"][0])
1830 if "bridgeheadServerListBL" in msg:
1831 for value in msg["bridgeheadServerListBL"]:
1832 dsdn = dsdb_Dn(samdb, value)
1833 dnstr = str(dsdn.dn)
1834 if dnstr not in self.bridgehead_list:
1835 self.bridgehead_list.append(dnstr)
1838 class RepsFromTo(object):
1839 """Class encapsulation of the NDR repsFromToBlob.
1841 Removes the necessity of external code having to
1842 understand about other_info or manipulation of
1843 update flags.
1845 def __init__(self, nc_dnstr=None, ndr_blob=None):
1847 self.__dict__['to_be_deleted'] = False
1848 self.__dict__['nc_dnstr'] = nc_dnstr
1849 self.__dict__['update_flags'] = 0x0
1850 # XXX the following sounds dubious and/or better solved
1851 # elsewhere, but lets leave it for now. In particular, there
1852 # seems to be no reason for all the non-ndr generated
1853 # attributes to be handled in the round about way (e.g.
1854 # self.__dict__['to_be_deleted'] = False above). On the other
1855 # hand, it all seems to work. Hooray! Hands off!.
1857 # WARNING:
1859 # There is a very subtle bug here with python
1860 # and our NDR code. If you assign directly to
1861 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1862 # then a proper python GC reference count is not
1863 # maintained.
1865 # To work around this we maintain an internal
1866 # reference to "dns_name(x)" and "other_info" elements
1867 # of repsFromToBlob. This internal reference
1868 # is hidden within this class but it is why you
1869 # see statements like this below:
1871 # self.__dict__['ndr_blob'].ctr.other_info = \
1872 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1874 # That would appear to be a redundant assignment but
1875 # it is necessary to hold a proper python GC reference
1876 # count.
1877 if ndr_blob is None:
1878 self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
1879 self.__dict__['ndr_blob'].version = 0x1
1880 self.__dict__['dns_name1'] = None
1881 self.__dict__['dns_name2'] = None
1883 self.__dict__['ndr_blob'].ctr.other_info = \
1884 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1886 else:
1887 self.__dict__['ndr_blob'] = ndr_blob
1888 self.__dict__['other_info'] = ndr_blob.ctr.other_info
1890 if ndr_blob.version == 0x1:
1891 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
1892 self.__dict__['dns_name2'] = None
1893 else:
1894 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
1895 self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
1897 def __str__(self):
1898 '''Debug dump string output of class'''
1900 text = "%s:" % self.__class__.__name__
1901 text += "\n\tdnstr=%s" % self.nc_dnstr
1902 text += "\n\tupdate_flags=0x%X" % self.update_flags
1903 text += "\n\tversion=%d" % self.version
1904 text += "\n\tsource_dsa_obj_guid=%s" % self.source_dsa_obj_guid
1905 text += ("\n\tsource_dsa_invocation_id=%s" %
1906 self.source_dsa_invocation_id)
1907 text += "\n\ttransport_guid=%s" % self.transport_guid
1908 text += "\n\treplica_flags=0x%X" % self.replica_flags
1909 text += ("\n\tconsecutive_sync_failures=%d" %
1910 self.consecutive_sync_failures)
1911 text += "\n\tlast_success=%s" % self.last_success
1912 text += "\n\tlast_attempt=%s" % self.last_attempt
1913 text += "\n\tdns_name1=%s" % self.dns_name1
1914 text += "\n\tdns_name2=%s" % self.dns_name2
1915 text += "\n\tschedule[ "
1916 for slot in self.schedule:
1917 text += "0x%X " % slot
1918 text += "]"
1920 return text
1922 def __setattr__(self, item, value):
1923 """Set an attribute and chyange update flag.
1925 Be aware that setting any RepsFromTo attribute will set the
1926 drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS update flag.
1928 if item in ['schedule', 'replica_flags', 'transport_guid',
1929 'source_dsa_obj_guid', 'source_dsa_invocation_id',
1930 'consecutive_sync_failures', 'last_success',
1931 'last_attempt']:
1933 if item in ['replica_flags']:
1934 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
1935 elif item in ['schedule']:
1936 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
1938 setattr(self.__dict__['ndr_blob'].ctr, item, value)
1940 elif item in ['dns_name1']:
1941 self.__dict__['dns_name1'] = value
1943 if self.__dict__['ndr_blob'].version == 0x1:
1944 self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
1945 self.__dict__['dns_name1']
1946 else:
1947 self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
1948 self.__dict__['dns_name1']
1950 elif item in ['dns_name2']:
1951 self.__dict__['dns_name2'] = value
1953 if self.__dict__['ndr_blob'].version == 0x1:
1954 raise AttributeError(item)
1955 else:
1956 self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
1957 self.__dict__['dns_name2']
1959 elif item in ['nc_dnstr']:
1960 self.__dict__['nc_dnstr'] = value
1962 elif item in ['to_be_deleted']:
1963 self.__dict__['to_be_deleted'] = value
1965 elif item in ['version']:
1966 raise AttributeError("Attempt to set readonly attribute %s" % item)
1967 else:
1968 raise AttributeError("Unknown attribute %s" % item)
1970 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
1972 def __getattr__(self, item):
1973 """Overload of RepsFromTo attribute retrieval.
1975 Allows external code to ignore substructures within the blob
1977 if item in ['schedule', 'replica_flags', 'transport_guid',
1978 'source_dsa_obj_guid', 'source_dsa_invocation_id',
1979 'consecutive_sync_failures', 'last_success',
1980 'last_attempt']:
1981 return getattr(self.__dict__['ndr_blob'].ctr, item)
1983 elif item in ['version']:
1984 return self.__dict__['ndr_blob'].version
1986 elif item in ['dns_name1']:
1987 if self.__dict__['ndr_blob'].version == 0x1:
1988 return self.__dict__['ndr_blob'].ctr.other_info.dns_name
1989 else:
1990 return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
1992 elif item in ['dns_name2']:
1993 if self.__dict__['ndr_blob'].version == 0x1:
1994 raise AttributeError(item)
1995 else:
1996 return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
1998 elif item in ['to_be_deleted']:
1999 return self.__dict__['to_be_deleted']
2001 elif item in ['nc_dnstr']:
2002 return self.__dict__['nc_dnstr']
2004 elif item in ['update_flags']:
2005 return self.__dict__['update_flags']
2007 raise AttributeError("Unknown attribute %s" % item)
2009 def is_modified(self):
2010 return (self.update_flags != 0x0)
2012 def set_unmodified(self):
2013 self.__dict__['update_flags'] = 0x0
2016 class SiteLink(object):
2017 """Class defines a site link found under sites
2020 def __init__(self, dnstr):
2021 self.dnstr = dnstr
2022 self.options = 0
2023 self.system_flags = 0
2024 self.cost = 0
2025 self.schedule = None
2026 self.interval = None
2027 self.site_list = []
2029 def __str__(self):
2030 '''Debug dump string output of Transport object'''
2032 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
2033 text = text + "\n\toptions=%d" % self.options
2034 text = text + "\n\tsystem_flags=%d" % self.system_flags
2035 text = text + "\n\tcost=%d" % self.cost
2036 text = text + "\n\tinterval=%s" % self.interval
2038 if self.schedule is not None:
2039 text += "\n\tschedule.size=%s" % self.schedule.size
2040 text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
2041 text += ("\n\tschedule.numberOfSchedules=%s" %
2042 self.schedule.numberOfSchedules)
2044 for i, header in enumerate(self.schedule.headerArray):
2045 text += ("\n\tschedule.headerArray[%d].type=%d" %
2046 (i, header.type))
2047 text += ("\n\tschedule.headerArray[%d].offset=%d" %
2048 (i, header.offset))
2049 text = text + "\n\tschedule.dataArray[%d].slots[ " % i
2050 for slot in self.schedule.dataArray[i].slots:
2051 text = text + "0x%X " % slot
2052 text = text + "]"
2054 for dnstr in self.site_list:
2055 text = text + "\n\tsite_list=%s" % dnstr
2056 return text
2058 def load_sitelink(self, samdb):
2059 """Given a siteLink object with an prior initialization
2060 for the object's DN, search for the DN and load attributes
2061 from the samdb.
2063 attrs = ["options",
2064 "systemFlags",
2065 "cost",
2066 "schedule",
2067 "replInterval",
2068 "siteList"]
2069 try:
2070 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
2071 attrs=attrs, controls=['extended_dn:0'])
2073 except ldb.LdbError, (enum, estr):
2074 raise Exception("Unable to find SiteLink for (%s) - (%s)" %
2075 (self.dnstr, estr))
2077 msg = res[0]
2079 if "options" in msg:
2080 self.options = int(msg["options"][0])
2082 if "systemFlags" in msg:
2083 self.system_flags = int(msg["systemFlags"][0])
2085 if "cost" in msg:
2086 self.cost = int(msg["cost"][0])
2088 if "replInterval" in msg:
2089 self.interval = int(msg["replInterval"][0])
2091 if "siteList" in msg:
2092 for value in msg["siteList"]:
2093 dsdn = dsdb_Dn(samdb, value)
2094 guid = misc.GUID(dsdn.dn.get_extended_component('GUID'))
2095 if guid not in self.site_list:
2096 self.site_list.append(guid)
2098 if "schedule" in msg:
2099 self.schedule = ndr_unpack(drsblobs.schedule, value)
2100 else:
2101 self.schedule = new_connection_schedule()
2104 class KCCFailedObject(object):
2105 def __init__(self, uuid, failure_count, time_first_failure,
2106 last_result, dns_name):
2107 self.uuid = uuid
2108 self.failure_count = failure_count
2109 self.time_first_failure = time_first_failure
2110 self.last_result = last_result
2111 self.dns_name = dns_name
2114 class ReplInfo(object):
2115 def __init__(self):
2116 self.cost = 0
2117 self.interval = 0
2118 self.options = 0
2119 self.schedule = None
2122 ##################################################
2123 # Global Functions and Variables
2124 ##################################################
2125 MAX_DWORD = 2 ** 32 - 1
2128 def get_dsa_config_rep(dsa):
2129 # Find configuration NC replica for the DSA
2130 for c_rep in dsa.current_rep_table.values():
2131 if c_rep.is_config():
2132 return c_rep
2134 raise KCCError("Unable to find config NC replica for (%s)" %
2135 dsa.dsa_dnstr)
2138 def sort_dsa_by_guid(dsa1, dsa2):
2139 "use ndr_pack for GUID comparison, as appears correct in some places"""
2140 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
2143 def total_schedule(schedule):
2144 if schedule is None:
2145 return 84 * 8 # 84 bytes = 84 * 8 bits
2147 total = 0
2148 for byte in schedule:
2149 while byte != 0:
2150 total += byte & 1
2151 byte >>= 1
2152 return total
2155 def new_connection_schedule():
2156 """Create a default schedule for an NTDSConnection or Sitelink. This
2157 is packed differently from the repltimes schedule used elsewhere
2158 in KCC (where the 168 nibbles are packed into 84 bytes).
2160 # 168 byte instances of the 0x01 value. The low order 4 bits
2161 # of the byte equate to 15 minute intervals within a single hour.
2162 # There are 168 bytes because there are 168 hours in a full week
2163 # Effectively we are saying to perform replication at the end of
2164 # each hour of the week
2165 schedule = drsblobs.schedule()
2167 schedule.size = 188
2168 schedule.bandwidth = 0
2169 schedule.numberOfSchedules = 1
2171 header = drsblobs.scheduleHeader()
2172 header.type = 0
2173 header.offset = 20
2175 schedule.headerArray = [header]
2177 data = drsblobs.scheduleSlots()
2178 data.slots = [0x01] * 168
2180 schedule.dataArray = [data]
2181 return schedule