nfs4acls: Use talloc_realloc()
[Samba.git] / python / samba / kcc / kcc_utils.py
blob3b5b0cce55d70dc1380446753bcb3c97b600e263
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 = dict((v, k) for k, v in NCType.__dict__.items() if k[:2] != '__')
47 class NamingContext(object):
48 """Base class for a naming context.
50 Holds the DN, GUID, SID (if available) and type of the DN.
51 Subclasses may inherit from this and specialize
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 # we don't use the search result, but it tests the status
1039 # of self.dnstr in the database.
1040 samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1042 except ldb.LdbError, (enum, estr):
1043 if enum == ldb.ERR_NO_SUCH_OBJECT:
1044 raise KCCError("nTDSConnection for (%s) doesn't exist!" %
1045 self.dnstr)
1046 raise KccError("Unable to search for (%s) - (%s)" %
1047 (self.dnstr, estr))
1049 if self.enabled:
1050 enablestr = "TRUE"
1051 else:
1052 enablestr = "FALSE"
1054 # Prepare a message for modifying the samdb
1055 m = ldb.Message()
1056 m.dn = ldb.Dn(samdb, self.dnstr)
1058 m["enabledConnection"] = \
1059 ldb.MessageElement(enablestr, ldb.FLAG_MOD_REPLACE,
1060 "enabledConnection")
1061 m["fromServer"] = \
1062 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_REPLACE,
1063 "fromServer")
1064 m["options"] = \
1065 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_REPLACE,
1066 "options")
1067 m["systemFlags"] = \
1068 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_REPLACE,
1069 "systemFlags")
1071 if self.transport_dnstr is not None:
1072 m["transportType"] = \
1073 ldb.MessageElement(str(self.transport_dnstr),
1074 ldb.FLAG_MOD_REPLACE, "transportType")
1075 else:
1076 m["transportType"] = \
1077 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "transportType")
1079 if self.schedule is not None:
1080 m["schedule"] = \
1081 ldb.MessageElement(ndr_pack(self.schedule),
1082 ldb.FLAG_MOD_REPLACE, "schedule")
1083 else:
1084 m["schedule"] = \
1085 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "schedule")
1086 try:
1087 samdb.modify(m)
1088 except ldb.LdbError, (enum, estr):
1089 raise Exception("Could not modify nTDSConnection for (%s) - (%s)" %
1090 (self.dnstr, estr))
1092 def set_modified(self, truefalse):
1093 self.to_be_modified = truefalse
1095 def set_added(self, truefalse):
1096 self.to_be_added = truefalse
1098 def set_deleted(self, truefalse):
1099 self.to_be_deleted = truefalse
1101 def is_schedule_minimum_once_per_week(self):
1102 """Returns True if our schedule includes at least one
1103 replication interval within the week. False otherwise
1105 # replinfo schedule is None means "always", while
1106 # NTDSConnection schedule is None means "never".
1107 if self.schedule is None or self.schedule.dataArray[0] is None:
1108 return False
1110 for slot in self.schedule.dataArray[0].slots:
1111 if (slot & 0x0F) != 0x0:
1112 return True
1113 return False
1115 def is_equivalent_schedule(self, sched):
1116 """Returns True if our schedule is equivalent to the input
1117 comparison schedule.
1119 :param shed: schedule to compare to
1121 if self.schedule is not None:
1122 if sched is None:
1123 return False
1124 elif sched is None:
1125 return True
1127 if ((self.schedule.size != sched.size or
1128 self.schedule.bandwidth != sched.bandwidth or
1129 self.schedule.numberOfSchedules != sched.numberOfSchedules)):
1130 return False
1132 for i, header in enumerate(self.schedule.headerArray):
1134 if self.schedule.headerArray[i].type != sched.headerArray[i].type:
1135 return False
1137 if self.schedule.headerArray[i].offset != \
1138 sched.headerArray[i].offset:
1139 return False
1141 for a, b in zip(self.schedule.dataArray[i].slots,
1142 sched.dataArray[i].slots):
1143 if a != b:
1144 return False
1145 return True
1147 def is_rodc_topology(self):
1148 """Returns True if NTDS Connection specifies RODC
1149 topology only
1151 if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
1152 return False
1153 return True
1155 def is_generated(self):
1156 """Returns True if NTDS Connection was generated by the
1157 KCC topology algorithm as opposed to set by the administrator
1159 if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0:
1160 return False
1161 return True
1163 def is_override_notify_default(self):
1164 """Returns True if NTDS Connection should override notify default
1166 if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0:
1167 return False
1168 return True
1170 def is_use_notify(self):
1171 """Returns True if NTDS Connection should use notify
1173 if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0:
1174 return False
1175 return True
1177 def is_twoway_sync(self):
1178 """Returns True if NTDS Connection should use twoway sync
1180 if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0:
1181 return False
1182 return True
1184 def is_intersite_compression_disabled(self):
1185 """Returns True if NTDS Connection intersite compression
1186 is disabled
1188 if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0:
1189 return False
1190 return True
1192 def is_user_owned_schedule(self):
1193 """Returns True if NTDS Connection has a user owned schedule
1195 if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0:
1196 return False
1197 return True
1199 def is_enabled(self):
1200 """Returns True if NTDS Connection is enabled
1202 return self.enabled
1204 def get_from_dnstr(self):
1205 '''Return fromServer dn string attribute'''
1206 return self.from_dnstr
1209 class Partition(NamingContext):
1210 """A naming context discovered thru Partitions DN of the config schema.
1212 This is a more specific form of NamingContext class (inheriting from that
1213 class) and it identifies unique attributes enumerated in the Partitions
1214 such as which nTDSDSAs are cross referenced for replicas
1216 def __init__(self, partstr):
1217 self.partstr = partstr
1218 self.enabled = True
1219 self.system_flags = 0
1220 self.rw_location_list = []
1221 self.ro_location_list = []
1223 # We don't have enough info to properly
1224 # fill in the naming context yet. We'll get that
1225 # fully set up with load_partition().
1226 NamingContext.__init__(self, None)
1228 def load_partition(self, samdb):
1229 """Given a Partition class object that has been initialized with its
1230 partition dn string, load the partition from the sam database, identify
1231 the type of the partition (schema, domain, etc) and record the list of
1232 nTDSDSAs that appear in the cross reference attributes
1233 msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations.
1235 :param samdb: sam database to load partition from
1237 attrs = ["nCName",
1238 "Enabled",
1239 "systemFlags",
1240 "msDS-NC-Replica-Locations",
1241 "msDS-NC-RO-Replica-Locations"]
1242 try:
1243 res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE,
1244 attrs=attrs)
1246 except ldb.LdbError, (enum, estr):
1247 raise Exception("Unable to find partition for (%s) - (%s)" % (
1248 self.partstr, estr))
1250 msg = res[0]
1251 for k in msg.keys():
1252 if k == "dn":
1253 continue
1255 if k == "Enabled":
1256 if msg[k][0].upper().lstrip().rstrip() == "TRUE":
1257 self.enabled = True
1258 else:
1259 self.enabled = False
1260 continue
1262 if k == "systemFlags":
1263 self.system_flags = int(msg[k][0])
1264 continue
1266 for value in msg[k]:
1267 dsdn = dsdb_Dn(samdb, value)
1268 dnstr = str(dsdn.dn)
1270 if k == "nCName":
1271 self.nc_dnstr = dnstr
1272 continue
1274 if k == "msDS-NC-Replica-Locations":
1275 self.rw_location_list.append(dnstr)
1276 continue
1278 if k == "msDS-NC-RO-Replica-Locations":
1279 self.ro_location_list.append(dnstr)
1280 continue
1282 # Now identify what type of NC this partition
1283 # enumerated
1284 self.identify_by_basedn(samdb)
1286 def is_enabled(self):
1287 """Returns True if partition is enabled
1289 return self.is_enabled
1291 def is_foreign(self):
1292 """Returns True if this is not an Active Directory NC in our
1293 forest but is instead something else (e.g. a foreign NC)
1295 if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0:
1296 return True
1297 else:
1298 return False
1300 def should_be_present(self, target_dsa):
1301 """Tests whether this partition should have an NC replica
1302 on the target dsa. This method returns a tuple of
1303 needed=True/False, ro=True/False, partial=True/False
1305 :param target_dsa: should NC be present on target dsa
1307 ro = False
1308 partial = False
1310 # If this is the config, schema, or default
1311 # domain NC for the target dsa then it should
1312 # be present
1313 needed = (self.nc_type == NCType.config or
1314 self.nc_type == NCType.schema or
1315 (self.nc_type == NCType.domain and
1316 self.nc_dnstr == target_dsa.default_dnstr))
1318 # A writable replica of an application NC should be present
1319 # if there a cross reference to the target DSA exists. Depending
1320 # on whether the DSA is ro we examine which type of cross reference
1321 # to look for (msDS-NC-Replica-Locations or
1322 # msDS-NC-RO-Replica-Locations
1323 if self.nc_type == NCType.application:
1324 if target_dsa.is_ro():
1325 if target_dsa.dsa_dnstr in self.ro_location_list:
1326 needed = True
1327 else:
1328 if target_dsa.dsa_dnstr in self.rw_location_list:
1329 needed = True
1331 # If the target dsa is a gc then a partial replica of a
1332 # domain NC (other than the DSAs default domain) should exist
1333 # if there is also a cross reference for the DSA
1334 if (target_dsa.is_gc() and
1335 self.nc_type == NCType.domain and
1336 self.nc_dnstr != target_dsa.default_dnstr and
1337 (target_dsa.dsa_dnstr in self.ro_location_list or
1338 target_dsa.dsa_dnstr in self.rw_location_list)):
1339 needed = True
1340 partial = True
1342 # partial NCs are always readonly
1343 if needed and (target_dsa.is_ro() or partial):
1344 ro = True
1346 return needed, ro, partial
1348 def __str__(self):
1349 '''Debug dump string output of class'''
1350 text = "%s" % NamingContext.__str__(self)
1351 text = text + "\n\tpartdn=%s" % self.partstr
1352 for k in self.rw_location_list:
1353 text = text + "\n\tmsDS-NC-Replica-Locations=%s" % k
1354 for k in self.ro_location_list:
1355 text = text + "\n\tmsDS-NC-RO-Replica-Locations=%s" % k
1356 return text
1359 class Site(object):
1360 """An individual site object discovered thru the configuration
1361 naming context. Contains all DSAs that exist within the site
1363 def __init__(self, site_dnstr, nt_now):
1364 self.site_dnstr = site_dnstr
1365 self.site_guid = None
1366 self.site_options = 0
1367 self.site_topo_generator = None
1368 self.site_topo_failover = 0 # appears to be in minutes
1369 self.dsa_table = {}
1370 self.rw_dsa_table = {}
1371 self.nt_now = nt_now
1373 def load_site(self, samdb):
1374 """Loads the NTDS Site Settions options attribute for the site
1375 as well as querying and loading all DSAs that appear within
1376 the site.
1378 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1379 attrs = ["options",
1380 "interSiteTopologyFailover",
1381 "interSiteTopologyGenerator"]
1382 try:
1383 res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE,
1384 attrs=attrs)
1385 self_res = samdb.search(base=self.site_dnstr, scope=ldb.SCOPE_BASE,
1386 attrs=['objectGUID'])
1387 except ldb.LdbError, (enum, estr):
1388 raise Exception("Unable to find site settings for (%s) - (%s)" %
1389 (ssdn, estr))
1391 msg = res[0]
1392 if "options" in msg:
1393 self.site_options = int(msg["options"][0])
1395 if "interSiteTopologyGenerator" in msg:
1396 self.site_topo_generator = \
1397 str(msg["interSiteTopologyGenerator"][0])
1399 if "interSiteTopologyFailover" in msg:
1400 self.site_topo_failover = int(msg["interSiteTopologyFailover"][0])
1402 msg = self_res[0]
1403 if "objectGUID" in msg:
1404 self.site_guid = misc.GUID(samdb.schema_format_value("objectGUID",
1405 msg["objectGUID"][0]))
1407 self.load_all_dsa(samdb)
1409 def load_all_dsa(self, samdb):
1410 """Discover all nTDSDSA thru the sites entry and
1411 instantiate and load the DSAs. Each dsa is inserted
1412 into the dsa_table by dn string.
1414 try:
1415 res = samdb.search(self.site_dnstr,
1416 scope=ldb.SCOPE_SUBTREE,
1417 expression="(objectClass=nTDSDSA)")
1418 except ldb.LdbError, (enum, estr):
1419 raise Exception("Unable to find nTDSDSAs - (%s)" % estr)
1421 for msg in res:
1422 dnstr = str(msg.dn)
1424 # already loaded
1425 if dnstr in self.dsa_table:
1426 continue
1428 dsa = DirectoryServiceAgent(dnstr)
1430 dsa.load_dsa(samdb)
1432 # Assign this dsa to my dsa table
1433 # and index by dsa dn
1434 self.dsa_table[dnstr] = dsa
1435 if not dsa.is_ro():
1436 self.rw_dsa_table[dnstr] = dsa
1438 def get_dsa_by_guidstr(self, guidstr): # XXX unused
1439 for dsa in self.dsa_table.values():
1440 if str(dsa.dsa_guid) == guidstr:
1441 return dsa
1442 return None
1444 def get_dsa(self, dnstr):
1445 """Return a previously loaded DSA object by consulting
1446 the sites dsa_table for the provided DSA dn string
1448 :return: None if DSA doesn't exist
1450 return self.dsa_table.get(dnstr)
1452 def select_istg(self, samdb, mydsa, ro):
1453 """Determine if my DC should be an intersite topology
1454 generator. If my DC is the istg and is both a writeable
1455 DC and the database is opened in write mode then we perform
1456 an originating update to set the interSiteTopologyGenerator
1457 attribute in the NTDS Site Settings object. An RODC always
1458 acts as an ISTG for itself.
1460 # The KCC on an RODC always acts as an ISTG for itself
1461 if mydsa.dsa_is_ro:
1462 mydsa.dsa_is_istg = True
1463 self.site_topo_generator = mydsa.dsa_dnstr
1464 return True
1466 c_rep = get_dsa_config_rep(mydsa)
1468 # Load repsFrom and replUpToDateVector if not already loaded
1469 # so we can get the current state of the config replica and
1470 # whether we are getting updates from the istg
1471 c_rep.load_repsFrom(samdb)
1473 c_rep.load_replUpToDateVector(samdb)
1475 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1476 # First, the KCC on a writable DC determines whether it acts
1477 # as an ISTG for its site
1479 # Let s be the object such that s!lDAPDisplayName = nTDSDSA
1480 # and classSchema in s!objectClass.
1482 # Let D be the sequence of objects o in the site of the local
1483 # DC such that o!objectCategory = s. D is sorted in ascending
1484 # order by objectGUID.
1486 # Which is a fancy way of saying "sort all the nTDSDSA objects
1487 # in the site by guid in ascending order". Place sorted list
1488 # in D_sort[]
1489 D_sort = sorted(self.rw_dsa_table.values(), cmp=sort_dsa_by_guid)
1491 # double word number of 100 nanosecond intervals since 1600s
1493 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1494 # if o!interSiteTopologyFailover is 0 or has no value.
1496 # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1497 # so it appears we have to turn f into the same interval
1499 # interSiteTopologyFailover (if set) appears to be in minutes
1500 # so we'll need to convert to senconds and then 100 nanosecond
1501 # intervals
1502 # XXX [MS-ADTS] 6.2.2.3.1 says it is seconds, not minutes.
1504 # 10,000,000 is number of 100 nanosecond intervals in a second
1505 if self.site_topo_failover == 0:
1506 f = 2 * 60 * 60 * 10000000
1507 else:
1508 f = self.site_topo_failover * 60 * 10000000
1510 # Let o be the site settings object for the site of the local
1511 # DC, or NULL if no such o exists.
1512 d_dsa = self.dsa_table.get(self.site_topo_generator)
1514 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1515 # If o != NULL and o!interSiteTopologyGenerator is not the
1516 # nTDSDSA object for the local DC and
1517 # o!interSiteTopologyGenerator is an element dj of sequence D:
1519 if d_dsa is not None and d_dsa is not mydsa:
1520 # From MS-ADTS 6.2.2.3.1 ISTG Selection:
1521 # Let c be the cursor in the replUpToDateVector variable
1522 # associated with the NC replica of the config NC such
1523 # that c.uuidDsa = dj!invocationId. If no such c exists
1524 # (No evidence of replication from current ITSG):
1525 # Let i = j.
1526 # Let t = 0.
1528 # Else if the current time < c.timeLastSyncSuccess - f
1529 # (Evidence of time sync problem on current ISTG):
1530 # Let i = 0.
1531 # Let t = 0.
1533 # Else (Evidence of replication from current ITSG):
1534 # Let i = j.
1535 # Let t = c.timeLastSyncSuccess.
1537 # last_success appears to be a double word containing
1538 # number of 100 nanosecond intervals since the 1600s
1539 j_idx = D_sort.index(d_dsa)
1541 found = False
1542 for cursor in c_rep.rep_replUpToDateVector_cursors:
1543 if d_dsa.dsa_ivid == cursor.source_dsa_invocation_id:
1544 found = True
1545 break
1547 if not found:
1548 i_idx = j_idx
1549 t_time = 0
1551 #XXX doc says current time < c.timeLastSyncSuccess - f
1552 # which is true only if f is negative or clocks are wrong.
1553 # f is not negative in the default case (2 hours).
1554 elif self.nt_now - cursor.last_sync_success > f:
1555 i_idx = 0
1556 t_time = 0
1557 else:
1558 i_idx = j_idx
1559 t_time = cursor.last_sync_success
1561 # Otherwise (Nominate local DC as ISTG):
1562 # Let i be the integer such that di is the nTDSDSA
1563 # object for the local DC.
1564 # Let t = the current time.
1565 else:
1566 i_idx = D_sort.index(mydsa)
1567 t_time = self.nt_now
1569 # Compute a function that maintains the current ISTG if
1570 # it is alive, cycles through other candidates if not.
1572 # Let k be the integer (i + ((current time - t) /
1573 # o!interSiteTopologyFailover)) MOD |D|.
1575 # Note: We don't want to divide by zero here so they must
1576 # have meant "f" instead of "o!interSiteTopologyFailover"
1577 k_idx = (i_idx + ((self.nt_now - t_time) / f)) % len(D_sort)
1579 # The local writable DC acts as an ISTG for its site if and
1580 # only if dk is the nTDSDSA object for the local DC. If the
1581 # local DC does not act as an ISTG, the KCC skips the
1582 # remainder of this task.
1583 d_dsa = D_sort[k_idx]
1584 d_dsa.dsa_is_istg = True
1586 # Update if we are the ISTG, otherwise return
1587 if d_dsa is not mydsa:
1588 return False
1590 # Nothing to do
1591 if self.site_topo_generator == mydsa.dsa_dnstr:
1592 return True
1594 self.site_topo_generator = mydsa.dsa_dnstr
1596 # If readonly database then do not perform a
1597 # persistent update
1598 if ro:
1599 return True
1601 # Perform update to the samdb
1602 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1604 m = ldb.Message()
1605 m.dn = ldb.Dn(samdb, ssdn)
1607 m["interSiteTopologyGenerator"] = \
1608 ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
1609 "interSiteTopologyGenerator")
1610 try:
1611 samdb.modify(m)
1613 except ldb.LdbError, estr:
1614 raise Exception(
1615 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1616 (ssdn, estr))
1617 return True
1619 def is_intrasite_topology_disabled(self):
1620 '''Returns True if intra-site topology is disabled for site'''
1621 return (self.site_options &
1622 dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0
1624 def is_intersite_topology_disabled(self):
1625 '''Returns True if inter-site topology is disabled for site'''
1626 return ((self.site_options &
1627 dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED)
1628 != 0)
1630 def is_random_bridgehead_disabled(self):
1631 '''Returns True if selection of random bridgehead is disabled'''
1632 return (self.site_options &
1633 dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0
1635 def is_detect_stale_disabled(self):
1636 '''Returns True if detect stale is disabled for site'''
1637 return (self.site_options &
1638 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0
1640 def is_cleanup_ntdsconn_disabled(self):
1641 '''Returns True if NTDS Connection cleanup is disabled for site'''
1642 return (self.site_options &
1643 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0
1645 def same_site(self, dsa):
1646 '''Return True if dsa is in this site'''
1647 if self.get_dsa(dsa.dsa_dnstr):
1648 return True
1649 return False
1651 def is_rodc_site(self):
1652 if len(self.dsa_table) > 0 and len(self.rw_dsa_table) == 0:
1653 return True
1654 return False
1656 def __str__(self):
1657 '''Debug dump string output of class'''
1658 text = "%s:" % self.__class__.__name__
1659 text = text + "\n\tdn=%s" % self.site_dnstr
1660 text = text + "\n\toptions=0x%X" % self.site_options
1661 text = text + "\n\ttopo_generator=%s" % self.site_topo_generator
1662 text = text + "\n\ttopo_failover=%d" % self.site_topo_failover
1663 for key, dsa in self.dsa_table.items():
1664 text = text + "\n%s" % dsa
1665 return text
1668 class GraphNode(object):
1669 """A graph node describing a set of edges that should be directed to it.
1671 Each edge is a connection for a particular naming context replica directed
1672 from another node in the forest to this node.
1675 def __init__(self, dsa_dnstr, max_node_edges):
1676 """Instantiate the graph node according to a DSA dn string
1678 :param max_node_edges: maximum number of edges that should ever
1679 be directed to the node
1681 self.max_edges = max_node_edges
1682 self.dsa_dnstr = dsa_dnstr
1683 self.edge_from = []
1685 def __str__(self):
1686 text = "%s:" % self.__class__.__name__
1687 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
1688 text = text + "\n\tmax_edges=%d" % self.max_edges
1690 for i, edge in enumerate(self.edge_from):
1691 text = text + "\n\tedge_from[%d]=%s" % (i, edge)
1692 return text
1694 def add_edge_from(self, from_dsa_dnstr):
1695 """Add an edge from the dsa to our graph nodes edge from list
1697 :param from_dsa_dnstr: the dsa that the edge emanates from
1699 assert from_dsa_dnstr is not None
1701 # No edges from myself to myself
1702 if from_dsa_dnstr == self.dsa_dnstr:
1703 return False
1704 # Only one edge from a particular node
1705 if from_dsa_dnstr in self.edge_from:
1706 return False
1707 # Not too many edges
1708 if len(self.edge_from) >= self.max_edges:
1709 return False
1710 self.edge_from.append(from_dsa_dnstr)
1711 return True
1713 def add_edges_from_connections(self, dsa):
1714 """For each nTDSConnection object associated with a particular
1715 DSA, we test if it implies an edge to this graph node (i.e.
1716 the "fromServer" attribute). If it does then we add an
1717 edge from the server unless we are over the max edges for this
1718 graph node
1720 :param dsa: dsa with a dnstr equivalent to his graph node
1722 for connect in dsa.connect_table.values():
1723 self.add_edge_from(connect.from_dnstr)
1725 def add_connections_from_edges(self, dsa):
1726 """For each edge directed to this graph node, ensure there
1727 is a corresponding nTDSConnection object in the dsa.
1729 for edge_dnstr in self.edge_from:
1730 connections = dsa.get_connection_by_from_dnstr(edge_dnstr)
1732 # For each edge directed to the NC replica that
1733 # "should be present" on the local DC, the KCC determines
1734 # whether an object c exists such that:
1736 # c is a child of the DC's nTDSDSA object.
1737 # c.objectCategory = nTDSConnection
1739 # Given the NC replica ri from which the edge is directed,
1740 # c.fromServer is the dsname of the nTDSDSA object of
1741 # the DC on which ri "is present".
1743 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1745 found_valid = False
1746 for connect in connections:
1747 if connect.is_rodc_topology():
1748 continue
1749 found_valid = True
1751 if found_valid:
1752 continue
1754 # if no such object exists then the KCC adds an object
1755 # c with the following attributes
1757 # Generate a new dnstr for this nTDSConnection
1758 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1759 flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1760 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1762 dsa.new_connection(opt, flags, None, edge_dnstr, None)
1764 def has_sufficient_edges(self):
1765 '''Return True if we have met the maximum "from edges" criteria'''
1766 if len(self.edge_from) >= self.max_edges:
1767 return True
1768 return False
1771 class Transport(object):
1772 """Class defines a Inter-site transport found under Sites
1775 def __init__(self, dnstr):
1776 self.dnstr = dnstr
1777 self.options = 0
1778 self.guid = None
1779 self.name = None
1780 self.address_attr = None
1781 self.bridgehead_list = []
1783 def __str__(self):
1784 '''Debug dump string output of Transport object'''
1786 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
1787 text = text + "\n\tguid=%s" % str(self.guid)
1788 text = text + "\n\toptions=%d" % self.options
1789 text = text + "\n\taddress_attr=%s" % self.address_attr
1790 text = text + "\n\tname=%s" % self.name
1791 for dnstr in self.bridgehead_list:
1792 text = text + "\n\tbridgehead_list=%s" % dnstr
1794 return text
1796 def load_transport(self, samdb):
1797 """Given a Transport object with an prior initialization
1798 for the object's DN, search for the DN and load attributes
1799 from the samdb.
1801 attrs = ["objectGUID",
1802 "options",
1803 "name",
1804 "bridgeheadServerListBL",
1805 "transportAddressAttribute"]
1806 try:
1807 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
1808 attrs=attrs)
1810 except ldb.LdbError, (enum, estr):
1811 raise Exception("Unable to find Transport for (%s) - (%s)" %
1812 (self.dnstr, estr))
1814 msg = res[0]
1815 self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
1816 msg["objectGUID"][0]))
1818 if "options" in msg:
1819 self.options = int(msg["options"][0])
1821 if "transportAddressAttribute" in msg:
1822 self.address_attr = str(msg["transportAddressAttribute"][0])
1824 if "name" in msg:
1825 self.name = str(msg["name"][0])
1827 if "bridgeheadServerListBL" in msg:
1828 for value in msg["bridgeheadServerListBL"]:
1829 dsdn = dsdb_Dn(samdb, value)
1830 dnstr = str(dsdn.dn)
1831 if dnstr not in self.bridgehead_list:
1832 self.bridgehead_list.append(dnstr)
1835 class RepsFromTo(object):
1836 """Class encapsulation of the NDR repsFromToBlob.
1838 Removes the necessity of external code having to
1839 understand about other_info or manipulation of
1840 update flags.
1842 def __init__(self, nc_dnstr=None, ndr_blob=None):
1844 self.__dict__['to_be_deleted'] = False
1845 self.__dict__['nc_dnstr'] = nc_dnstr
1846 self.__dict__['update_flags'] = 0x0
1847 # XXX the following sounds dubious and/or better solved
1848 # elsewhere, but lets leave it for now. In particular, there
1849 # seems to be no reason for all the non-ndr generated
1850 # attributes to be handled in the round about way (e.g.
1851 # self.__dict__['to_be_deleted'] = False above). On the other
1852 # hand, it all seems to work. Hooray! Hands off!.
1854 # WARNING:
1856 # There is a very subtle bug here with python
1857 # and our NDR code. If you assign directly to
1858 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1859 # then a proper python GC reference count is not
1860 # maintained.
1862 # To work around this we maintain an internal
1863 # reference to "dns_name(x)" and "other_info" elements
1864 # of repsFromToBlob. This internal reference
1865 # is hidden within this class but it is why you
1866 # see statements like this below:
1868 # self.__dict__['ndr_blob'].ctr.other_info = \
1869 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1871 # That would appear to be a redundant assignment but
1872 # it is necessary to hold a proper python GC reference
1873 # count.
1874 if ndr_blob is None:
1875 self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
1876 self.__dict__['ndr_blob'].version = 0x1
1877 self.__dict__['dns_name1'] = None
1878 self.__dict__['dns_name2'] = None
1880 self.__dict__['ndr_blob'].ctr.other_info = \
1881 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1883 else:
1884 self.__dict__['ndr_blob'] = ndr_blob
1885 self.__dict__['other_info'] = ndr_blob.ctr.other_info
1887 if ndr_blob.version == 0x1:
1888 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
1889 self.__dict__['dns_name2'] = None
1890 else:
1891 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
1892 self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
1894 def __str__(self):
1895 '''Debug dump string output of class'''
1897 text = "%s:" % self.__class__.__name__
1898 text += "\n\tdnstr=%s" % self.nc_dnstr
1899 text += "\n\tupdate_flags=0x%X" % self.update_flags
1900 text += "\n\tversion=%d" % self.version
1901 text += "\n\tsource_dsa_obj_guid=%s" % self.source_dsa_obj_guid
1902 text += ("\n\tsource_dsa_invocation_id=%s" %
1903 self.source_dsa_invocation_id)
1904 text += "\n\ttransport_guid=%s" % self.transport_guid
1905 text += "\n\treplica_flags=0x%X" % self.replica_flags
1906 text += ("\n\tconsecutive_sync_failures=%d" %
1907 self.consecutive_sync_failures)
1908 text += "\n\tlast_success=%s" % self.last_success
1909 text += "\n\tlast_attempt=%s" % self.last_attempt
1910 text += "\n\tdns_name1=%s" % self.dns_name1
1911 text += "\n\tdns_name2=%s" % self.dns_name2
1912 text += "\n\tschedule[ "
1913 for slot in self.schedule:
1914 text += "0x%X " % slot
1915 text += "]"
1917 return text
1919 def __setattr__(self, item, value):
1920 """Set an attribute and chyange update flag.
1922 Be aware that setting any RepsFromTo attribute will set the
1923 drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS update flag.
1925 if item in ['schedule', 'replica_flags', 'transport_guid',
1926 'source_dsa_obj_guid', 'source_dsa_invocation_id',
1927 'consecutive_sync_failures', 'last_success',
1928 'last_attempt']:
1930 if item in ['replica_flags']:
1931 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
1932 elif item in ['schedule']:
1933 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
1935 setattr(self.__dict__['ndr_blob'].ctr, item, value)
1937 elif item in ['dns_name1']:
1938 self.__dict__['dns_name1'] = value
1940 if self.__dict__['ndr_blob'].version == 0x1:
1941 self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
1942 self.__dict__['dns_name1']
1943 else:
1944 self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
1945 self.__dict__['dns_name1']
1947 elif item in ['dns_name2']:
1948 self.__dict__['dns_name2'] = value
1950 if self.__dict__['ndr_blob'].version == 0x1:
1951 raise AttributeError(item)
1952 else:
1953 self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
1954 self.__dict__['dns_name2']
1956 elif item in ['nc_dnstr']:
1957 self.__dict__['nc_dnstr'] = value
1959 elif item in ['to_be_deleted']:
1960 self.__dict__['to_be_deleted'] = value
1962 elif item in ['version']:
1963 raise AttributeError("Attempt to set readonly attribute %s" % item)
1964 else:
1965 raise AttributeError("Unknown attribute %s" % item)
1967 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
1969 def __getattr__(self, item):
1970 """Overload of RepsFromTo attribute retrieval.
1972 Allows external code to ignore substructures within the blob
1974 if item in ['schedule', 'replica_flags', 'transport_guid',
1975 'source_dsa_obj_guid', 'source_dsa_invocation_id',
1976 'consecutive_sync_failures', 'last_success',
1977 'last_attempt']:
1978 return getattr(self.__dict__['ndr_blob'].ctr, item)
1980 elif item in ['version']:
1981 return self.__dict__['ndr_blob'].version
1983 elif item in ['dns_name1']:
1984 if self.__dict__['ndr_blob'].version == 0x1:
1985 return self.__dict__['ndr_blob'].ctr.other_info.dns_name
1986 else:
1987 return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
1989 elif item in ['dns_name2']:
1990 if self.__dict__['ndr_blob'].version == 0x1:
1991 raise AttributeError(item)
1992 else:
1993 return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
1995 elif item in ['to_be_deleted']:
1996 return self.__dict__['to_be_deleted']
1998 elif item in ['nc_dnstr']:
1999 return self.__dict__['nc_dnstr']
2001 elif item in ['update_flags']:
2002 return self.__dict__['update_flags']
2004 raise AttributeError("Unknown attribute %s" % item)
2006 def is_modified(self):
2007 return (self.update_flags != 0x0)
2009 def set_unmodified(self):
2010 self.__dict__['update_flags'] = 0x0
2013 class SiteLink(object):
2014 """Class defines a site link found under sites
2017 def __init__(self, dnstr):
2018 self.dnstr = dnstr
2019 self.options = 0
2020 self.system_flags = 0
2021 self.cost = 0
2022 self.schedule = None
2023 self.interval = None
2024 self.site_list = []
2026 def __str__(self):
2027 '''Debug dump string output of Transport object'''
2029 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
2030 text = text + "\n\toptions=%d" % self.options
2031 text = text + "\n\tsystem_flags=%d" % self.system_flags
2032 text = text + "\n\tcost=%d" % self.cost
2033 text = text + "\n\tinterval=%s" % self.interval
2035 if self.schedule is not None:
2036 text += "\n\tschedule.size=%s" % self.schedule.size
2037 text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
2038 text += ("\n\tschedule.numberOfSchedules=%s" %
2039 self.schedule.numberOfSchedules)
2041 for i, header in enumerate(self.schedule.headerArray):
2042 text += ("\n\tschedule.headerArray[%d].type=%d" %
2043 (i, header.type))
2044 text += ("\n\tschedule.headerArray[%d].offset=%d" %
2045 (i, header.offset))
2046 text = text + "\n\tschedule.dataArray[%d].slots[ " % i
2047 for slot in self.schedule.dataArray[i].slots:
2048 text = text + "0x%X " % slot
2049 text = text + "]"
2051 for dnstr in self.site_list:
2052 text = text + "\n\tsite_list=%s" % dnstr
2053 return text
2055 def load_sitelink(self, samdb):
2056 """Given a siteLink object with an prior initialization
2057 for the object's DN, search for the DN and load attributes
2058 from the samdb.
2060 attrs = ["options",
2061 "systemFlags",
2062 "cost",
2063 "schedule",
2064 "replInterval",
2065 "siteList"]
2066 try:
2067 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
2068 attrs=attrs, controls=['extended_dn:0'])
2070 except ldb.LdbError, (enum, estr):
2071 raise Exception("Unable to find SiteLink for (%s) - (%s)" %
2072 (self.dnstr, estr))
2074 msg = res[0]
2076 if "options" in msg:
2077 self.options = int(msg["options"][0])
2079 if "systemFlags" in msg:
2080 self.system_flags = int(msg["systemFlags"][0])
2082 if "cost" in msg:
2083 self.cost = int(msg["cost"][0])
2085 if "replInterval" in msg:
2086 self.interval = int(msg["replInterval"][0])
2088 if "siteList" in msg:
2089 for value in msg["siteList"]:
2090 dsdn = dsdb_Dn(samdb, value)
2091 guid = misc.GUID(dsdn.dn.get_extended_component('GUID'))
2092 if guid not in self.site_list:
2093 self.site_list.append(guid)
2095 if "schedule" in msg:
2096 self.schedule = ndr_unpack(drsblobs.schedule, value)
2097 else:
2098 self.schedule = new_connection_schedule()
2101 class KCCFailedObject(object):
2102 def __init__(self, uuid, failure_count, time_first_failure,
2103 last_result, dns_name):
2104 self.uuid = uuid
2105 self.failure_count = failure_count
2106 self.time_first_failure = time_first_failure
2107 self.last_result = last_result
2108 self.dns_name = dns_name
2111 ##################################################
2112 # Global Functions and Variables
2113 ##################################################
2115 def get_dsa_config_rep(dsa):
2116 # Find configuration NC replica for the DSA
2117 for c_rep in dsa.current_rep_table.values():
2118 if c_rep.is_config():
2119 return c_rep
2121 raise KCCError("Unable to find config NC replica for (%s)" %
2122 dsa.dsa_dnstr)
2125 def sort_dsa_by_guid(dsa1, dsa2):
2126 "use ndr_pack for GUID comparison, as appears correct in some places"""
2127 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
2130 def new_connection_schedule():
2131 """Create a default schedule for an NTDSConnection or Sitelink. This
2132 is packed differently from the repltimes schedule used elsewhere
2133 in KCC (where the 168 nibbles are packed into 84 bytes).
2135 # 168 byte instances of the 0x01 value. The low order 4 bits
2136 # of the byte equate to 15 minute intervals within a single hour.
2137 # There are 168 bytes because there are 168 hours in a full week
2138 # Effectively we are saying to perform replication at the end of
2139 # each hour of the week
2140 schedule = drsblobs.schedule()
2142 schedule.size = 188
2143 schedule.bandwidth = 0
2144 schedule.numberOfSchedules = 1
2146 header = drsblobs.scheduleHeader()
2147 header.type = 0
2148 header.offset = 20
2150 schedule.headerArray = [header]
2152 data = drsblobs.scheduleSlots()
2153 data.slots = [0x01] * 168
2155 schedule.dataArray = [data]
2156 return schedule