s4:rpc_server: ignore CO_CANCEL and ORPHANED PDUs
[Samba.git] / python / samba / kcc / kcc_utils.py
blob1d4b9d61f18a4f6db7453fe57d5ee9cdd2335738
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 KCCError("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 # RepsFromTo tuples
200 self.rep_repsTo = []
202 # The (is present) test is a combination of being
203 # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or
204 # hasPartialReplicaNCs) as well as its replica flags found
205 # thru the msDS-HasInstantiatedNCs. If the NC replica meets
206 # the first enumeration test then this flag is set true
207 self.rep_present_criteria_one = False
209 # Call my super class we inherited from
210 NamingContext.__init__(self, nc_dnstr)
212 def __str__(self):
213 '''Debug dump string output of class'''
214 text = "%s:" % self.__class__.__name__
215 text = text + "\n\tdsa_dnstr=%s" % self.rep_dsa_dnstr
216 text = text + "\n\tdsa_guid=%s" % self.rep_dsa_guid
217 text = text + "\n\tdefault=%s" % self.rep_default
218 text = text + "\n\tro=%s" % self.rep_ro
219 text = text + "\n\tpartial=%s" % self.rep_partial
220 text = text + "\n\tpresent=%s" % self.is_present()
221 text = text + "\n\tfsmo_role_owner=%s" % self.rep_fsmo_role_owner
223 for rep in self.rep_repsFrom:
224 text = text + "\n%s" % rep
226 for rep in self.rep_repsTo:
227 text = text + "\n%s" % rep
229 return "%s\n%s" % (NamingContext.__str__(self), text)
231 def set_instantiated_flags(self, flags=None):
232 '''Set or clear NC replica instantiated flags'''
233 if flags is None:
234 self.rep_instantiated_flags = 0
235 else:
236 self.rep_instantiated_flags = flags
238 def identify_by_dsa_attr(self, samdb, attr):
239 """Given an NC which has been discovered thru the
240 nTDSDSA database object, determine what type of NC
241 replica it is (i.e. partial, read only, default)
243 :param attr: attr of nTDSDSA object where NC DN appears
245 # If the NC was found under hasPartialReplicaNCs
246 # then a partial replica at this dsa
247 if attr == "hasPartialReplicaNCs":
248 self.rep_partial = True
249 self.rep_present_criteria_one = True
251 # If the NC is listed under msDS-HasDomainNCs then
252 # this can only be a domain NC and it is the DSA's
253 # default domain NC
254 elif attr == "msDS-HasDomainNCs":
255 self.rep_default = True
257 # NCs listed under hasMasterNCs are either
258 # default domain, schema, or config. We check
259 # against schema and config because they will be
260 # the same for all nTDSDSAs in the forest. That
261 # leaves the default domain NC remaining which
262 # may be different for each nTDSDSAs (and thus
263 # we don't compare agains this samdb's default
264 # basedn
265 elif attr == "hasMasterNCs":
266 self.rep_present_criteria_one = True
268 if self.nc_dnstr != str(samdb.get_schema_basedn()) and \
269 self.nc_dnstr != str(samdb.get_config_basedn()):
270 self.rep_default = True
272 # RODC only
273 elif attr == "msDS-hasFullReplicaNCs":
274 self.rep_present_criteria_one = True
275 self.rep_ro = True
277 # Not RODC
278 elif attr == "msDS-hasMasterNCs":
279 self.rep_present_criteria_one = True
280 self.rep_ro = False
282 # Now use this DSA attribute to identify the naming
283 # context type by calling the super class method
284 # of the same name
285 NamingContext.identify_by_dsa_attr(self, samdb, attr)
287 def is_default(self):
288 """Whether this is a default domain for the dsa that this NC appears on
290 return self.rep_default
292 def is_ro(self):
293 '''Return True if NC replica is read only'''
294 return self.rep_ro
296 def is_partial(self):
297 '''Return True if NC replica is partial'''
298 return self.rep_partial
300 def is_present(self):
301 """Given an NC replica which has been discovered thru the
302 nTDSDSA database object and populated with replica flags
303 from the msDS-HasInstantiatedNCs; return whether the NC
304 replica is present (true) or if the IT_NC_GOING flag is
305 set then the NC replica is not present (false)
307 if self.rep_present_criteria_one and \
308 self.rep_instantiated_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0:
309 return True
310 return False
312 def load_repsFrom(self, samdb):
313 """Given an NC replica which has been discovered thru the nTDSDSA
314 database object, load the repsFrom attribute for the local replica.
315 held by my dsa. The repsFrom attribute is not replicated so this
316 attribute is relative only to the local DSA that the samdb exists on
318 try:
319 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
320 attrs=["repsFrom"])
322 except ldb.LdbError, (enum, estr):
323 raise KCCError("Unable to find NC for (%s) - (%s)" %
324 (self.nc_dnstr, estr))
326 msg = res[0]
328 # Possibly no repsFrom if this is a singleton DC
329 if "repsFrom" in msg:
330 for value in msg["repsFrom"]:
331 rep = RepsFromTo(self.nc_dnstr,
332 ndr_unpack(drsblobs.repsFromToBlob, value))
333 self.rep_repsFrom.append(rep)
335 def commit_repsFrom(self, samdb, ro=False):
336 """Commit repsFrom to the database"""
338 # XXX - This is not truly correct according to the MS-TECH
339 # docs. To commit a repsFrom we should be using RPCs
340 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
341 # IDL_DRSReplicaDel to affect a repsFrom change.
343 # Those RPCs are missing in samba, so I'll have to
344 # implement them to get this to more accurately
345 # reflect the reference docs. As of right now this
346 # commit to the database will work as its what the
347 # older KCC also did
348 modify = False
349 newreps = []
350 delreps = []
352 for repsFrom in self.rep_repsFrom:
354 # Leave out any to be deleted from
355 # replacement list. Build a list
356 # of to be deleted reps which we will
357 # remove from rep_repsFrom list below
358 if repsFrom.to_be_deleted:
359 delreps.append(repsFrom)
360 modify = True
361 continue
363 if repsFrom.is_modified():
364 repsFrom.set_unmodified()
365 modify = True
367 # current (unmodified) elements also get
368 # appended here but no changes will occur
369 # unless something is "to be modified" or
370 # "to be deleted"
371 newreps.append(ndr_pack(repsFrom.ndr_blob))
373 # Now delete these from our list of rep_repsFrom
374 for repsFrom in delreps:
375 self.rep_repsFrom.remove(repsFrom)
376 delreps = []
378 # Nothing to do if no reps have been modified or
379 # need to be deleted or input option has informed
380 # us to be "readonly" (ro). Leave database
381 # record "as is"
382 if not modify or ro:
383 return
385 m = ldb.Message()
386 m.dn = ldb.Dn(samdb, self.nc_dnstr)
388 m["repsFrom"] = \
389 ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsFrom")
391 try:
392 samdb.modify(m)
394 except ldb.LdbError, estr:
395 raise KCCError("Could not set repsFrom for (%s) - (%s)" %
396 (self.nc_dnstr, estr))
398 def load_replUpToDateVector(self, samdb):
399 """Given an NC replica which has been discovered thru the nTDSDSA
400 database object, load the replUpToDateVector attribute for the
401 local replica. held by my dsa. The replUpToDateVector
402 attribute is not replicated so this attribute is relative only
403 to the local DSA that the samdb exists on
406 try:
407 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
408 attrs=["replUpToDateVector"])
410 except ldb.LdbError, (enum, estr):
411 raise KCCError("Unable to find NC for (%s) - (%s)" %
412 (self.nc_dnstr, estr))
414 msg = res[0]
416 # Possibly no replUpToDateVector if this is a singleton DC
417 if "replUpToDateVector" in msg:
418 value = msg["replUpToDateVector"][0]
419 blob = ndr_unpack(drsblobs.replUpToDateVectorBlob,
420 value)
421 if blob.version != 2:
422 # Samba only generates version 2, and this runs locally
423 raise AttributeError("Unexpected replUpToDateVector version %d"
424 % blob.version)
426 self.rep_replUpToDateVector_cursors = blob.ctr.cursors
427 else:
428 self.rep_replUpToDateVector_cursors = []
430 def dumpstr_to_be_deleted(self):
431 return '\n'.join(str(x) for x in self.rep_repsFrom if x.to_be_deleted)
433 def dumpstr_to_be_modified(self):
434 return '\n'.join(str(x) for x in self.rep_repsFrom if x.is_modified())
436 def dumpstr_reps_to(self):
437 return '\n'.join(str(x) for x in self.rep_repsTo if x.to_be_deleted)
439 def load_fsmo_roles(self, samdb):
440 """Given an NC replica which has been discovered thru the nTDSDSA
441 database object, load the fSMORoleOwner attribute.
443 try:
444 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
445 attrs=["fSMORoleOwner"])
447 except ldb.LdbError, (enum, estr):
448 raise KCCError("Unable to find NC for (%s) - (%s)" %
449 (self.nc_dnstr, estr))
451 msg = res[0]
453 # Possibly no fSMORoleOwner
454 if "fSMORoleOwner" in msg:
455 self.rep_fsmo_role_owner = msg["fSMORoleOwner"]
457 def is_fsmo_role_owner(self, dsa_dnstr):
458 if self.rep_fsmo_role_owner is not None and \
459 self.rep_fsmo_role_owner == dsa_dnstr:
460 return True
461 return False
463 def load_repsTo(self, samdb):
464 """Given an NC replica which has been discovered thru the nTDSDSA
465 database object, load the repsTo attribute for the local replica.
466 held by my dsa. The repsTo attribute is not replicated so this
467 attribute is relative only to the local DSA that the samdb exists on
469 This is responsible for push replication, not scheduled pull
470 replication. Not to be confused for repsFrom.
472 try:
473 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
474 attrs=["repsTo"])
476 except ldb.LdbError, (enum, estr):
477 raise KCCError("Unable to find NC for (%s) - (%s)" %
478 (self.nc_dnstr, estr))
480 msg = res[0]
482 # Possibly no repsTo if this is a singleton DC
483 if "repsTo" in msg:
484 for value in msg["repsTo"]:
485 rep = RepsFromTo(self.nc_dnstr,
486 ndr_unpack(drsblobs.repsFromToBlob, value))
487 self.rep_repsTo.append(rep)
489 def commit_repsTo(self, samdb, ro=False):
490 """Commit repsTo to the database"""
492 # XXX - This is not truly correct according to the MS-TECH
493 # docs. To commit a repsTo we should be using RPCs
494 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
495 # IDL_DRSReplicaDel to affect a repsTo change.
497 # Those RPCs are missing in samba, so I'll have to
498 # implement them to get this to more accurately
499 # reflect the reference docs. As of right now this
500 # commit to the database will work as its what the
501 # older KCC also did
502 modify = False
503 newreps = []
504 delreps = []
506 for repsTo in self.rep_repsTo:
508 # Leave out any to be deleted from
509 # replacement list. Build a list
510 # of to be deleted reps which we will
511 # remove from rep_repsTo list below
512 if repsTo.to_be_deleted:
513 delreps.append(repsTo)
514 modify = True
515 continue
517 if repsTo.is_modified():
518 repsTo.set_unmodified()
519 modify = True
521 # current (unmodified) elements also get
522 # appended here but no changes will occur
523 # unless something is "to be modified" or
524 # "to be deleted"
525 newreps.append(ndr_pack(repsTo.ndr_blob))
527 # Now delete these from our list of rep_repsTo
528 for repsTo in delreps:
529 self.rep_repsTo.remove(repsTo)
530 delreps = []
532 # Nothing to do if no reps have been modified or
533 # need to be deleted or input option has informed
534 # us to be "readonly" (ro). Leave database
535 # record "as is"
536 if not modify or ro:
537 return
539 m = ldb.Message()
540 m.dn = ldb.Dn(samdb, self.nc_dnstr)
542 m["repsTo"] = \
543 ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsTo")
545 try:
546 samdb.modify(m)
548 except ldb.LdbError, estr:
549 raise KCCError("Could not set repsTo for (%s) - (%s)" %
550 (self.nc_dnstr, estr))
553 class DirectoryServiceAgent(object):
555 def __init__(self, dsa_dnstr):
556 """Initialize DSA class.
558 Class is subsequently fully populated by calling the load_dsa() method
560 :param dsa_dnstr: DN of the nTDSDSA
562 self.dsa_dnstr = dsa_dnstr
563 self.dsa_guid = None
564 self.dsa_ivid = None
565 self.dsa_is_ro = False
566 self.dsa_is_istg = False
567 self.options = 0
568 self.dsa_behavior = 0
569 self.default_dnstr = None # default domain dn string for dsa
571 # NCReplicas for this dsa that are "present"
572 # Indexed by DN string of naming context
573 self.current_rep_table = {}
575 # NCReplicas for this dsa that "should be present"
576 # Indexed by DN string of naming context
577 self.needed_rep_table = {}
579 # NTDSConnections for this dsa. These are current
580 # valid connections that are committed or pending a commit
581 # in the database. Indexed by DN string of connection
582 self.connect_table = {}
584 def __str__(self):
585 '''Debug dump string output of class'''
587 text = "%s:" % self.__class__.__name__
588 if self.dsa_dnstr is not None:
589 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
590 if self.dsa_guid is not None:
591 text = text + "\n\tdsa_guid=%s" % str(self.dsa_guid)
592 if self.dsa_ivid is not None:
593 text = text + "\n\tdsa_ivid=%s" % str(self.dsa_ivid)
595 text = text + "\n\tro=%s" % self.is_ro()
596 text = text + "\n\tgc=%s" % self.is_gc()
597 text = text + "\n\tistg=%s" % self.is_istg()
599 text = text + "\ncurrent_replica_table:"
600 text = text + "\n%s" % self.dumpstr_current_replica_table()
601 text = text + "\nneeded_replica_table:"
602 text = text + "\n%s" % self.dumpstr_needed_replica_table()
603 text = text + "\nconnect_table:"
604 text = text + "\n%s" % self.dumpstr_connect_table()
606 return text
608 def get_current_replica(self, nc_dnstr):
609 return self.current_rep_table.get(nc_dnstr)
611 def is_istg(self):
612 '''Returns True if dsa is intersite topology generator for it's site'''
613 # The KCC on an RODC always acts as an ISTG for itself
614 return self.dsa_is_istg or self.dsa_is_ro
616 def is_ro(self):
617 '''Returns True if dsa a read only domain controller'''
618 return self.dsa_is_ro
620 def is_gc(self):
621 '''Returns True if dsa hosts a global catalog'''
622 if (self.options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
623 return True
624 return False
626 def is_minimum_behavior(self, version):
627 """Is dsa at minimum windows level greater than or equal to (version)
629 :param version: Windows version to test against
630 (e.g. DS_DOMAIN_FUNCTION_2008)
632 if self.dsa_behavior >= version:
633 return True
634 return False
636 def is_translate_ntdsconn_disabled(self):
637 """Whether this allows NTDSConnection translation in its options."""
638 if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0:
639 return True
640 return False
642 def get_rep_tables(self):
643 """Return DSA current and needed replica tables
645 return self.current_rep_table, self.needed_rep_table
647 def get_parent_dnstr(self):
648 """Get the parent DN string of this object."""
649 head, sep, tail = self.dsa_dnstr.partition(',')
650 return tail
652 def load_dsa(self, samdb):
653 """Load a DSA from the samdb.
655 Prior initialization has given us the DN of the DSA that we are to
656 load. This method initializes all other attributes, including loading
657 the NC replica table for this DSA.
659 attrs = ["objectGUID",
660 "invocationID",
661 "options",
662 "msDS-isRODC",
663 "msDS-Behavior-Version"]
664 try:
665 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
666 attrs=attrs)
668 except ldb.LdbError, (enum, estr):
669 raise KCCError("Unable to find nTDSDSA for (%s) - (%s)" %
670 (self.dsa_dnstr, estr))
672 msg = res[0]
673 self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID",
674 msg["objectGUID"][0]))
676 # RODCs don't originate changes and thus have no invocationId,
677 # therefore we must check for existence first
678 if "invocationId" in msg:
679 self.dsa_ivid = misc.GUID(samdb.schema_format_value("objectGUID",
680 msg["invocationId"][0]))
682 if "options" in msg:
683 self.options = int(msg["options"][0])
685 if "msDS-isRODC" in msg and msg["msDS-isRODC"][0] == "TRUE":
686 self.dsa_is_ro = True
687 else:
688 self.dsa_is_ro = False
690 if "msDS-Behavior-Version" in msg:
691 self.dsa_behavior = int(msg['msDS-Behavior-Version'][0])
693 # Load the NC replicas that are enumerated on this dsa
694 self.load_current_replica_table(samdb)
696 # Load the nTDSConnection that are enumerated on this dsa
697 self.load_connection_table(samdb)
699 def load_current_replica_table(self, samdb):
700 """Method to load the NC replica's listed for DSA object.
702 This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs,
703 hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, and
704 msDS-HasInstantiatedNCs) to determine complete list of NC replicas that
705 are enumerated for the DSA. Once a NC replica is loaded it is
706 identified (schema, config, etc) and the other replica attributes
707 (partial, ro, etc) are determined.
709 :param samdb: database to query for DSA replica list
711 ncattrs = [
712 # not RODC - default, config, schema (old style)
713 "hasMasterNCs",
714 # not RODC - default, config, schema, app NCs
715 "msDS-hasMasterNCs",
716 # domain NC partial replicas
717 "hasPartialReplicaNCs",
718 # default domain NC
719 "msDS-HasDomainNCs",
720 # RODC only - default, config, schema, app NCs
721 "msDS-hasFullReplicaNCs",
722 # Identifies if replica is coming, going, or stable
723 "msDS-HasInstantiatedNCs"
725 try:
726 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
727 attrs=ncattrs)
729 except ldb.LdbError, (enum, estr):
730 raise KCCError("Unable to find nTDSDSA NCs for (%s) - (%s)" %
731 (self.dsa_dnstr, estr))
733 # The table of NCs for the dsa we are searching
734 tmp_table = {}
736 # We should get one response to our query here for
737 # the ntds that we requested
738 if len(res[0]) > 0:
740 # Our response will contain a number of elements including
741 # the dn of the dsa as well as elements for each
742 # attribute (e.g. hasMasterNCs). Each of these elements
743 # is a dictonary list which we retrieve the keys for and
744 # then iterate over them
745 for k in res[0].keys():
746 if k == "dn":
747 continue
749 # For each attribute type there will be one or more DNs
750 # listed. For instance DCs normally have 3 hasMasterNCs
751 # listed.
752 for value in res[0][k]:
753 # Turn dn into a dsdb_Dn so we can use
754 # its methods to parse a binary DN
755 dsdn = dsdb_Dn(samdb, value)
756 flags = dsdn.get_binary_integer()
757 dnstr = str(dsdn.dn)
759 if not dnstr in tmp_table:
760 rep = NCReplica(self.dsa_dnstr, self.dsa_guid, dnstr)
761 tmp_table[dnstr] = rep
762 else:
763 rep = tmp_table[dnstr]
765 if k == "msDS-HasInstantiatedNCs":
766 rep.set_instantiated_flags(flags)
767 continue
769 rep.identify_by_dsa_attr(samdb, k)
771 # if we've identified the default domain NC
772 # then save its DN string
773 if rep.is_default():
774 self.default_dnstr = dnstr
775 else:
776 raise KCCError("No nTDSDSA NCs for (%s)" % self.dsa_dnstr)
778 # Assign our newly built NC replica table to this dsa
779 self.current_rep_table = tmp_table
781 def add_needed_replica(self, rep):
782 """Method to add a NC replica that "should be present" to the
783 needed_rep_table.
785 self.needed_rep_table[rep.nc_dnstr] = rep
787 def load_connection_table(self, samdb):
788 """Method to load the nTDSConnections listed for DSA object.
790 :param samdb: database to query for DSA connection list
792 try:
793 res = samdb.search(base=self.dsa_dnstr,
794 scope=ldb.SCOPE_SUBTREE,
795 expression="(objectClass=nTDSConnection)")
797 except ldb.LdbError, (enum, estr):
798 raise KCCError("Unable to find nTDSConnection for (%s) - (%s)" %
799 (self.dsa_dnstr, estr))
801 for msg in res:
802 dnstr = str(msg.dn)
804 # already loaded
805 if dnstr in self.connect_table:
806 continue
808 connect = NTDSConnection(dnstr)
810 connect.load_connection(samdb)
811 self.connect_table[dnstr] = connect
813 def commit_connections(self, samdb, ro=False):
814 """Method to commit any uncommitted nTDSConnections
815 modifications that are in our table. These would be
816 identified connections that are marked to be added or
817 deleted
819 :param samdb: database to commit DSA connection list to
820 :param ro: if (true) then peform internal operations but
821 do not write to the database (readonly)
823 delconn = []
825 for dnstr, connect in self.connect_table.items():
826 if connect.to_be_added:
827 connect.commit_added(samdb, ro)
829 if connect.to_be_modified:
830 connect.commit_modified(samdb, ro)
832 if connect.to_be_deleted:
833 connect.commit_deleted(samdb, ro)
834 delconn.append(dnstr)
836 # Now delete the connection from the table
837 for dnstr in delconn:
838 del self.connect_table[dnstr]
840 def add_connection(self, dnstr, connect):
841 assert dnstr not in self.connect_table
842 self.connect_table[dnstr] = connect
844 def get_connection_by_from_dnstr(self, from_dnstr):
845 """Scan DSA nTDSConnection table and return connection
846 with a "fromServer" dn string equivalent to method
847 input parameter.
849 :param from_dnstr: search for this from server entry
851 answer = []
852 for connect in self.connect_table.values():
853 if connect.get_from_dnstr() == from_dnstr:
854 answer.append(connect)
856 return answer
858 def dumpstr_current_replica_table(self):
859 '''Debug dump string output of current replica table'''
860 return '\n'.join(str(x) for x in self.current_rep_table)
862 def dumpstr_needed_replica_table(self):
863 '''Debug dump string output of needed replica table'''
864 return '\n'.join(str(x) for x in self.needed_rep_table)
866 def dumpstr_connect_table(self):
867 '''Debug dump string output of connect table'''
868 return '\n'.join(str(x) for x in self.connect_table)
870 def new_connection(self, options, system_flags, transport, from_dnstr,
871 sched):
872 """Set up a new connection for the DSA based on input
873 parameters. Connection will be added to the DSA
874 connect_table and will be marked as "to be added" pending
875 a call to commit_connections()
877 dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
879 connect = NTDSConnection(dnstr)
880 connect.to_be_added = True
881 connect.enabled = True
882 connect.from_dnstr = from_dnstr
883 connect.options = options
884 connect.system_flags = system_flags
886 if transport is not None:
887 connect.transport_dnstr = transport.dnstr
888 connect.transport_guid = transport.guid
890 if sched is not None:
891 connect.schedule = sched
892 else:
893 # Create schedule. Attribute valuse set according to MS-TECH
894 # intrasite connection creation document
895 connect.schedule = new_connection_schedule()
897 self.add_connection(dnstr, connect)
898 return connect
901 class NTDSConnection(object):
902 """Class defines a nTDSConnection found under a DSA
904 def __init__(self, dnstr):
905 self.dnstr = dnstr
906 self.guid = None
907 self.enabled = False
908 self.whenCreated = 0
909 self.to_be_added = False # new connection needs to be added
910 self.to_be_deleted = False # old connection needs to be deleted
911 self.to_be_modified = False
912 self.options = 0
913 self.system_flags = 0
914 self.transport_dnstr = None
915 self.transport_guid = None
916 self.from_dnstr = None
917 self.schedule = None
919 def __str__(self):
920 '''Debug dump string output of NTDSConnection object'''
922 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
923 text = text + "\n\tenabled=%s" % self.enabled
924 text = text + "\n\tto_be_added=%s" % self.to_be_added
925 text = text + "\n\tto_be_deleted=%s" % self.to_be_deleted
926 text = text + "\n\tto_be_modified=%s" % self.to_be_modified
927 text = text + "\n\toptions=0x%08X" % self.options
928 text = text + "\n\tsystem_flags=0x%08X" % self.system_flags
929 text = text + "\n\twhenCreated=%d" % self.whenCreated
930 text = text + "\n\ttransport_dn=%s" % self.transport_dnstr
932 if self.guid is not None:
933 text = text + "\n\tguid=%s" % str(self.guid)
935 if self.transport_guid is not None:
936 text = text + "\n\ttransport_guid=%s" % str(self.transport_guid)
938 text = text + "\n\tfrom_dn=%s" % self.from_dnstr
940 if self.schedule is not None:
941 text += "\n\tschedule.size=%s" % self.schedule.size
942 text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
943 text += ("\n\tschedule.numberOfSchedules=%s" %
944 self.schedule.numberOfSchedules)
946 for i, header in enumerate(self.schedule.headerArray):
947 text += ("\n\tschedule.headerArray[%d].type=%d" %
948 (i, header.type))
949 text += ("\n\tschedule.headerArray[%d].offset=%d" %
950 (i, header.offset))
951 text += "\n\tschedule.dataArray[%d].slots[ " % i
952 for slot in self.schedule.dataArray[i].slots:
953 text = text + "0x%X " % slot
954 text = text + "]"
956 return text
958 def load_connection(self, samdb):
959 """Given a NTDSConnection object with an prior initialization
960 for the object's DN, search for the DN and load attributes
961 from the samdb.
963 attrs = ["options",
964 "enabledConnection",
965 "schedule",
966 "whenCreated",
967 "objectGUID",
968 "transportType",
969 "fromServer",
970 "systemFlags"]
971 try:
972 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
973 attrs=attrs)
975 except ldb.LdbError, (enum, estr):
976 raise KCCError("Unable to find nTDSConnection for (%s) - (%s)" %
977 (self.dnstr, estr))
979 msg = res[0]
981 if "options" in msg:
982 self.options = int(msg["options"][0])
984 if "enabledConnection" in msg:
985 if msg["enabledConnection"][0].upper().lstrip().rstrip() == "TRUE":
986 self.enabled = True
988 if "systemFlags" in msg:
989 self.system_flags = int(msg["systemFlags"][0])
991 try:
992 self.guid = \
993 misc.GUID(samdb.schema_format_value("objectGUID",
994 msg["objectGUID"][0]))
995 except KeyError:
996 raise KCCError("Unable to find objectGUID in nTDSConnection "
997 "for (%s)" % (self.dnstr))
999 if "transportType" in msg:
1000 dsdn = dsdb_Dn(samdb, msg["transportType"][0])
1001 self.load_connection_transport(samdb, str(dsdn.dn))
1003 if "schedule" in msg:
1004 self.schedule = ndr_unpack(drsblobs.schedule, msg["schedule"][0])
1006 if "whenCreated" in msg:
1007 self.whenCreated = ldb.string_to_time(msg["whenCreated"][0])
1009 if "fromServer" in msg:
1010 dsdn = dsdb_Dn(samdb, msg["fromServer"][0])
1011 self.from_dnstr = str(dsdn.dn)
1012 assert self.from_dnstr is not None
1014 def load_connection_transport(self, samdb, tdnstr):
1015 """Given a NTDSConnection object which enumerates a transport
1016 DN, load the transport information for the connection object
1018 :param tdnstr: transport DN to load
1020 attrs = ["objectGUID"]
1021 try:
1022 res = samdb.search(base=tdnstr,
1023 scope=ldb.SCOPE_BASE, attrs=attrs)
1025 except ldb.LdbError, (enum, estr):
1026 raise KCCError("Unable to find transport (%s) - (%s)" %
1027 (tdnstr, estr))
1029 if "objectGUID" in res[0]:
1030 msg = res[0]
1031 self.transport_dnstr = tdnstr
1032 self.transport_guid = \
1033 misc.GUID(samdb.schema_format_value("objectGUID",
1034 msg["objectGUID"][0]))
1035 assert self.transport_dnstr is not None
1036 assert self.transport_guid is not None
1038 def commit_deleted(self, samdb, ro=False):
1039 """Local helper routine for commit_connections() which
1040 handles committed connections that are to be deleted from
1041 the database database
1043 assert self.to_be_deleted
1044 self.to_be_deleted = False
1046 # No database modification requested
1047 if ro:
1048 return
1050 try:
1051 samdb.delete(self.dnstr)
1052 except ldb.LdbError, (enum, estr):
1053 raise KCCError("Could not delete nTDSConnection for (%s) - (%s)" %
1054 (self.dnstr, estr))
1056 def commit_added(self, samdb, ro=False):
1057 """Local helper routine for commit_connections() which
1058 handles committed connections that are to be added to the
1059 database
1061 assert self.to_be_added
1062 self.to_be_added = False
1064 # No database modification requested
1065 if ro:
1066 return
1068 # First verify we don't have this entry to ensure nothing
1069 # is programatically amiss
1070 found = False
1071 try:
1072 msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1073 if len(msg) != 0:
1074 found = True
1076 except ldb.LdbError, (enum, estr):
1077 if enum != ldb.ERR_NO_SUCH_OBJECT:
1078 raise KCCError("Unable to search for (%s) - (%s)" %
1079 (self.dnstr, estr))
1080 if found:
1081 raise KCCError("nTDSConnection for (%s) already exists!" %
1082 self.dnstr)
1084 if self.enabled:
1085 enablestr = "TRUE"
1086 else:
1087 enablestr = "FALSE"
1089 # Prepare a message for adding to the samdb
1090 m = ldb.Message()
1091 m.dn = ldb.Dn(samdb, self.dnstr)
1093 m["objectClass"] = \
1094 ldb.MessageElement("nTDSConnection", ldb.FLAG_MOD_ADD,
1095 "objectClass")
1096 m["showInAdvancedViewOnly"] = \
1097 ldb.MessageElement("TRUE", ldb.FLAG_MOD_ADD,
1098 "showInAdvancedViewOnly")
1099 m["enabledConnection"] = \
1100 ldb.MessageElement(enablestr, ldb.FLAG_MOD_ADD,
1101 "enabledConnection")
1102 m["fromServer"] = \
1103 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_ADD, "fromServer")
1104 m["options"] = \
1105 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options")
1106 m["systemFlags"] = \
1107 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_ADD,
1108 "systemFlags")
1110 if self.transport_dnstr is not None:
1111 m["transportType"] = \
1112 ldb.MessageElement(str(self.transport_dnstr), ldb.FLAG_MOD_ADD,
1113 "transportType")
1115 if self.schedule is not None:
1116 m["schedule"] = \
1117 ldb.MessageElement(ndr_pack(self.schedule),
1118 ldb.FLAG_MOD_ADD, "schedule")
1119 try:
1120 samdb.add(m)
1121 except ldb.LdbError, (enum, estr):
1122 raise KCCError("Could not add nTDSConnection for (%s) - (%s)" %
1123 (self.dnstr, estr))
1125 def commit_modified(self, samdb, ro=False):
1126 """Local helper routine for commit_connections() which
1127 handles committed connections that are to be modified to the
1128 database
1130 assert self.to_be_modified
1131 self.to_be_modified = False
1133 # No database modification requested
1134 if ro:
1135 return
1137 # First verify we have this entry to ensure nothing
1138 # is programatically amiss
1139 try:
1140 # we don't use the search result, but it tests the status
1141 # of self.dnstr in the database.
1142 samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1144 except ldb.LdbError, (enum, estr):
1145 if enum == ldb.ERR_NO_SUCH_OBJECT:
1146 raise KCCError("nTDSConnection for (%s) doesn't exist!" %
1147 self.dnstr)
1148 raise KCCError("Unable to search for (%s) - (%s)" %
1149 (self.dnstr, estr))
1151 if self.enabled:
1152 enablestr = "TRUE"
1153 else:
1154 enablestr = "FALSE"
1156 # Prepare a message for modifying the samdb
1157 m = ldb.Message()
1158 m.dn = ldb.Dn(samdb, self.dnstr)
1160 m["enabledConnection"] = \
1161 ldb.MessageElement(enablestr, ldb.FLAG_MOD_REPLACE,
1162 "enabledConnection")
1163 m["fromServer"] = \
1164 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_REPLACE,
1165 "fromServer")
1166 m["options"] = \
1167 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_REPLACE,
1168 "options")
1169 m["systemFlags"] = \
1170 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_REPLACE,
1171 "systemFlags")
1173 if self.transport_dnstr is not None:
1174 m["transportType"] = \
1175 ldb.MessageElement(str(self.transport_dnstr),
1176 ldb.FLAG_MOD_REPLACE, "transportType")
1177 else:
1178 m["transportType"] = \
1179 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "transportType")
1181 if self.schedule is not None:
1182 m["schedule"] = \
1183 ldb.MessageElement(ndr_pack(self.schedule),
1184 ldb.FLAG_MOD_REPLACE, "schedule")
1185 else:
1186 m["schedule"] = \
1187 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "schedule")
1188 try:
1189 samdb.modify(m)
1190 except ldb.LdbError, (enum, estr):
1191 raise KCCError("Could not modify nTDSConnection for (%s) - (%s)" %
1192 (self.dnstr, estr))
1194 def set_modified(self, truefalse):
1195 self.to_be_modified = truefalse
1197 def is_schedule_minimum_once_per_week(self):
1198 """Returns True if our schedule includes at least one
1199 replication interval within the week. False otherwise
1201 # replinfo schedule is None means "always", while
1202 # NTDSConnection schedule is None means "never".
1203 if self.schedule is None or self.schedule.dataArray[0] is None:
1204 return False
1206 for slot in self.schedule.dataArray[0].slots:
1207 if (slot & 0x0F) != 0x0:
1208 return True
1209 return False
1211 def is_equivalent_schedule(self, sched):
1212 """Returns True if our schedule is equivalent to the input
1213 comparison schedule.
1215 :param shed: schedule to compare to
1217 # There are 4 cases, where either self.schedule or sched can be None
1219 # | self. is None | self. is not None
1220 # --------------+-----------------+--------------------
1221 # sched is None | True | False
1222 # --------------+-----------------+--------------------
1223 # sched is not None | False | do calculations
1225 if self.schedule is None:
1226 return sched is None
1228 if sched is None:
1229 return False
1231 if ((self.schedule.size != sched.size or
1232 self.schedule.bandwidth != sched.bandwidth or
1233 self.schedule.numberOfSchedules != sched.numberOfSchedules)):
1234 return False
1236 for i, header in enumerate(self.schedule.headerArray):
1238 if self.schedule.headerArray[i].type != sched.headerArray[i].type:
1239 return False
1241 if self.schedule.headerArray[i].offset != \
1242 sched.headerArray[i].offset:
1243 return False
1245 for a, b in zip(self.schedule.dataArray[i].slots,
1246 sched.dataArray[i].slots):
1247 if a != b:
1248 return False
1249 return True
1251 def is_rodc_topology(self):
1252 """Returns True if NTDS Connection specifies RODC
1253 topology only
1255 if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
1256 return False
1257 return True
1259 def is_generated(self):
1260 """Returns True if NTDS Connection was generated by the
1261 KCC topology algorithm as opposed to set by the administrator
1263 if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0:
1264 return False
1265 return True
1267 def is_override_notify_default(self):
1268 """Returns True if NTDS Connection should override notify default
1270 if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0:
1271 return False
1272 return True
1274 def is_use_notify(self):
1275 """Returns True if NTDS Connection should use notify
1277 if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0:
1278 return False
1279 return True
1281 def is_twoway_sync(self):
1282 """Returns True if NTDS Connection should use twoway sync
1284 if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0:
1285 return False
1286 return True
1288 def is_intersite_compression_disabled(self):
1289 """Returns True if NTDS Connection intersite compression
1290 is disabled
1292 if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0:
1293 return False
1294 return True
1296 def is_user_owned_schedule(self):
1297 """Returns True if NTDS Connection has a user owned schedule
1299 if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0:
1300 return False
1301 return True
1303 def is_enabled(self):
1304 """Returns True if NTDS Connection is enabled
1306 return self.enabled
1308 def get_from_dnstr(self):
1309 '''Return fromServer dn string attribute'''
1310 return self.from_dnstr
1313 class Partition(NamingContext):
1314 """A naming context discovered thru Partitions DN of the config schema.
1316 This is a more specific form of NamingContext class (inheriting from that
1317 class) and it identifies unique attributes enumerated in the Partitions
1318 such as which nTDSDSAs are cross referenced for replicas
1320 def __init__(self, partstr):
1321 self.partstr = partstr
1322 self.enabled = True
1323 self.system_flags = 0
1324 self.rw_location_list = []
1325 self.ro_location_list = []
1327 # We don't have enough info to properly
1328 # fill in the naming context yet. We'll get that
1329 # fully set up with load_partition().
1330 NamingContext.__init__(self, None)
1332 def load_partition(self, samdb):
1333 """Given a Partition class object that has been initialized with its
1334 partition dn string, load the partition from the sam database, identify
1335 the type of the partition (schema, domain, etc) and record the list of
1336 nTDSDSAs that appear in the cross reference attributes
1337 msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations.
1339 :param samdb: sam database to load partition from
1341 attrs = ["nCName",
1342 "Enabled",
1343 "systemFlags",
1344 "msDS-NC-Replica-Locations",
1345 "msDS-NC-RO-Replica-Locations"]
1346 try:
1347 res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE,
1348 attrs=attrs)
1350 except ldb.LdbError, (enum, estr):
1351 raise KCCError("Unable to find partition for (%s) - (%s)" %
1352 (self.partstr, estr))
1353 msg = res[0]
1354 for k in msg.keys():
1355 if k == "dn":
1356 continue
1358 if k == "Enabled":
1359 if msg[k][0].upper().lstrip().rstrip() == "TRUE":
1360 self.enabled = True
1361 else:
1362 self.enabled = False
1363 continue
1365 if k == "systemFlags":
1366 self.system_flags = int(msg[k][0])
1367 continue
1369 for value in msg[k]:
1370 dsdn = dsdb_Dn(samdb, value)
1371 dnstr = str(dsdn.dn)
1373 if k == "nCName":
1374 self.nc_dnstr = dnstr
1375 continue
1377 if k == "msDS-NC-Replica-Locations":
1378 self.rw_location_list.append(dnstr)
1379 continue
1381 if k == "msDS-NC-RO-Replica-Locations":
1382 self.ro_location_list.append(dnstr)
1383 continue
1385 # Now identify what type of NC this partition
1386 # enumerated
1387 self.identify_by_basedn(samdb)
1389 def is_enabled(self):
1390 """Returns True if partition is enabled
1392 return self.is_enabled
1394 def is_foreign(self):
1395 """Returns True if this is not an Active Directory NC in our
1396 forest but is instead something else (e.g. a foreign NC)
1398 if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0:
1399 return True
1400 else:
1401 return False
1403 def should_be_present(self, target_dsa):
1404 """Tests whether this partition should have an NC replica
1405 on the target dsa. This method returns a tuple of
1406 needed=True/False, ro=True/False, partial=True/False
1408 :param target_dsa: should NC be present on target dsa
1410 ro = False
1411 partial = False
1413 # If this is the config, schema, or default
1414 # domain NC for the target dsa then it should
1415 # be present
1416 needed = (self.nc_type == NCType.config or
1417 self.nc_type == NCType.schema or
1418 (self.nc_type == NCType.domain and
1419 self.nc_dnstr == target_dsa.default_dnstr))
1421 # A writable replica of an application NC should be present
1422 # if there a cross reference to the target DSA exists. Depending
1423 # on whether the DSA is ro we examine which type of cross reference
1424 # to look for (msDS-NC-Replica-Locations or
1425 # msDS-NC-RO-Replica-Locations
1426 if self.nc_type == NCType.application:
1427 if target_dsa.is_ro():
1428 if target_dsa.dsa_dnstr in self.ro_location_list:
1429 needed = True
1430 else:
1431 if target_dsa.dsa_dnstr in self.rw_location_list:
1432 needed = True
1434 # If the target dsa is a gc then a partial replica of a
1435 # domain NC (other than the DSAs default domain) should exist
1436 # if there is also a cross reference for the DSA
1437 if (target_dsa.is_gc() and
1438 self.nc_type == NCType.domain and
1439 self.nc_dnstr != target_dsa.default_dnstr and
1440 (target_dsa.dsa_dnstr in self.ro_location_list or
1441 target_dsa.dsa_dnstr in self.rw_location_list)):
1442 needed = True
1443 partial = True
1445 # partial NCs are always readonly
1446 if needed and (target_dsa.is_ro() or partial):
1447 ro = True
1449 return needed, ro, partial
1451 def __str__(self):
1452 '''Debug dump string output of class'''
1453 text = "%s" % NamingContext.__str__(self)
1454 text = text + "\n\tpartdn=%s" % self.partstr
1455 for k in self.rw_location_list:
1456 text = text + "\n\tmsDS-NC-Replica-Locations=%s" % k
1457 for k in self.ro_location_list:
1458 text = text + "\n\tmsDS-NC-RO-Replica-Locations=%s" % k
1459 return text
1462 class Site(object):
1463 """An individual site object discovered thru the configuration
1464 naming context. Contains all DSAs that exist within the site
1466 def __init__(self, site_dnstr, nt_now):
1467 self.site_dnstr = site_dnstr
1468 self.site_guid = None
1469 self.site_options = 0
1470 self.site_topo_generator = None
1471 self.site_topo_failover = 0 # appears to be in minutes
1472 self.dsa_table = {}
1473 self.rw_dsa_table = {}
1474 self.nt_now = nt_now
1476 def load_site(self, samdb):
1477 """Loads the NTDS Site Settings options attribute for the site
1478 as well as querying and loading all DSAs that appear within
1479 the site.
1481 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1482 attrs = ["options",
1483 "interSiteTopologyFailover",
1484 "interSiteTopologyGenerator"]
1485 try:
1486 res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE,
1487 attrs=attrs)
1488 self_res = samdb.search(base=self.site_dnstr, scope=ldb.SCOPE_BASE,
1489 attrs=['objectGUID'])
1490 except ldb.LdbError, (enum, estr):
1491 raise KCCError("Unable to find site settings for (%s) - (%s)" %
1492 (ssdn, estr))
1494 msg = res[0]
1495 if "options" in msg:
1496 self.site_options = int(msg["options"][0])
1498 if "interSiteTopologyGenerator" in msg:
1499 self.site_topo_generator = \
1500 str(msg["interSiteTopologyGenerator"][0])
1502 if "interSiteTopologyFailover" in msg:
1503 self.site_topo_failover = int(msg["interSiteTopologyFailover"][0])
1505 msg = self_res[0]
1506 if "objectGUID" in msg:
1507 self.site_guid = misc.GUID(samdb.schema_format_value("objectGUID",
1508 msg["objectGUID"][0]))
1510 self.load_all_dsa(samdb)
1512 def load_all_dsa(self, samdb):
1513 """Discover all nTDSDSA thru the sites entry and
1514 instantiate and load the DSAs. Each dsa is inserted
1515 into the dsa_table by dn string.
1517 try:
1518 res = samdb.search(self.site_dnstr,
1519 scope=ldb.SCOPE_SUBTREE,
1520 expression="(objectClass=nTDSDSA)")
1521 except ldb.LdbError, (enum, estr):
1522 raise KCCError("Unable to find nTDSDSAs - (%s)" % estr)
1524 for msg in res:
1525 dnstr = str(msg.dn)
1527 # already loaded
1528 if dnstr in self.dsa_table:
1529 continue
1531 dsa = DirectoryServiceAgent(dnstr)
1533 dsa.load_dsa(samdb)
1535 # Assign this dsa to my dsa table
1536 # and index by dsa dn
1537 self.dsa_table[dnstr] = dsa
1538 if not dsa.is_ro():
1539 self.rw_dsa_table[dnstr] = dsa
1541 def get_dsa_by_guidstr(self, guidstr): # XXX unused
1542 for dsa in self.dsa_table.values():
1543 if str(dsa.dsa_guid) == guidstr:
1544 return dsa
1545 return None
1547 def get_dsa(self, dnstr):
1548 """Return a previously loaded DSA object by consulting
1549 the sites dsa_table for the provided DSA dn string
1551 :return: None if DSA doesn't exist
1553 return self.dsa_table.get(dnstr)
1555 def select_istg(self, samdb, mydsa, ro):
1556 """Determine if my DC should be an intersite topology
1557 generator. If my DC is the istg and is both a writeable
1558 DC and the database is opened in write mode then we perform
1559 an originating update to set the interSiteTopologyGenerator
1560 attribute in the NTDS Site Settings object. An RODC always
1561 acts as an ISTG for itself.
1563 # The KCC on an RODC always acts as an ISTG for itself
1564 if mydsa.dsa_is_ro:
1565 mydsa.dsa_is_istg = True
1566 self.site_topo_generator = mydsa.dsa_dnstr
1567 return True
1569 c_rep = get_dsa_config_rep(mydsa)
1571 # Load repsFrom and replUpToDateVector if not already loaded
1572 # so we can get the current state of the config replica and
1573 # whether we are getting updates from the istg
1574 c_rep.load_repsFrom(samdb)
1576 c_rep.load_replUpToDateVector(samdb)
1578 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1579 # First, the KCC on a writable DC determines whether it acts
1580 # as an ISTG for its site
1582 # Let s be the object such that s!lDAPDisplayName = nTDSDSA
1583 # and classSchema in s!objectClass.
1585 # Let D be the sequence of objects o in the site of the local
1586 # DC such that o!objectCategory = s. D is sorted in ascending
1587 # order by objectGUID.
1589 # Which is a fancy way of saying "sort all the nTDSDSA objects
1590 # in the site by guid in ascending order". Place sorted list
1591 # in D_sort[]
1592 D_sort = sorted(self.rw_dsa_table.values(), cmp=sort_dsa_by_guid)
1594 # double word number of 100 nanosecond intervals since 1600s
1596 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1597 # if o!interSiteTopologyFailover is 0 or has no value.
1599 # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1600 # so it appears we have to turn f into the same interval
1602 # interSiteTopologyFailover (if set) appears to be in minutes
1603 # so we'll need to convert to senconds and then 100 nanosecond
1604 # intervals
1605 # XXX [MS-ADTS] 6.2.2.3.1 says it is seconds, not minutes.
1607 # 10,000,000 is number of 100 nanosecond intervals in a second
1608 if self.site_topo_failover == 0:
1609 f = 2 * 60 * 60 * 10000000
1610 else:
1611 f = self.site_topo_failover * 60 * 10000000
1613 # Let o be the site settings object for the site of the local
1614 # DC, or NULL if no such o exists.
1615 d_dsa = self.dsa_table.get(self.site_topo_generator)
1617 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1618 # If o != NULL and o!interSiteTopologyGenerator is not the
1619 # nTDSDSA object for the local DC and
1620 # o!interSiteTopologyGenerator is an element dj of sequence D:
1622 if d_dsa is not None and d_dsa is not mydsa:
1623 # From MS-ADTS 6.2.2.3.1 ISTG Selection:
1624 # Let c be the cursor in the replUpToDateVector variable
1625 # associated with the NC replica of the config NC such
1626 # that c.uuidDsa = dj!invocationId. If no such c exists
1627 # (No evidence of replication from current ITSG):
1628 # Let i = j.
1629 # Let t = 0.
1631 # Else if the current time < c.timeLastSyncSuccess - f
1632 # (Evidence of time sync problem on current ISTG):
1633 # Let i = 0.
1634 # Let t = 0.
1636 # Else (Evidence of replication from current ITSG):
1637 # Let i = j.
1638 # Let t = c.timeLastSyncSuccess.
1640 # last_success appears to be a double word containing
1641 # number of 100 nanosecond intervals since the 1600s
1642 j_idx = D_sort.index(d_dsa)
1644 found = False
1645 for cursor in c_rep.rep_replUpToDateVector_cursors:
1646 if d_dsa.dsa_ivid == cursor.source_dsa_invocation_id:
1647 found = True
1648 break
1650 if not found:
1651 i_idx = j_idx
1652 t_time = 0
1654 #XXX doc says current time < c.timeLastSyncSuccess - f
1655 # which is true only if f is negative or clocks are wrong.
1656 # f is not negative in the default case (2 hours).
1657 elif self.nt_now - cursor.last_sync_success > f:
1658 i_idx = 0
1659 t_time = 0
1660 else:
1661 i_idx = j_idx
1662 t_time = cursor.last_sync_success
1664 # Otherwise (Nominate local DC as ISTG):
1665 # Let i be the integer such that di is the nTDSDSA
1666 # object for the local DC.
1667 # Let t = the current time.
1668 else:
1669 i_idx = D_sort.index(mydsa)
1670 t_time = self.nt_now
1672 # Compute a function that maintains the current ISTG if
1673 # it is alive, cycles through other candidates if not.
1675 # Let k be the integer (i + ((current time - t) /
1676 # o!interSiteTopologyFailover)) MOD |D|.
1678 # Note: We don't want to divide by zero here so they must
1679 # have meant "f" instead of "o!interSiteTopologyFailover"
1680 k_idx = (i_idx + ((self.nt_now - t_time) / f)) % len(D_sort)
1682 # The local writable DC acts as an ISTG for its site if and
1683 # only if dk is the nTDSDSA object for the local DC. If the
1684 # local DC does not act as an ISTG, the KCC skips the
1685 # remainder of this task.
1686 d_dsa = D_sort[k_idx]
1687 d_dsa.dsa_is_istg = True
1689 # Update if we are the ISTG, otherwise return
1690 if d_dsa is not mydsa:
1691 return False
1693 # Nothing to do
1694 if self.site_topo_generator == mydsa.dsa_dnstr:
1695 return True
1697 self.site_topo_generator = mydsa.dsa_dnstr
1699 # If readonly database then do not perform a
1700 # persistent update
1701 if ro:
1702 return True
1704 # Perform update to the samdb
1705 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1707 m = ldb.Message()
1708 m.dn = ldb.Dn(samdb, ssdn)
1710 m["interSiteTopologyGenerator"] = \
1711 ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
1712 "interSiteTopologyGenerator")
1713 try:
1714 samdb.modify(m)
1716 except ldb.LdbError, estr:
1717 raise KCCError(
1718 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1719 (ssdn, estr))
1720 return True
1722 def is_intrasite_topology_disabled(self):
1723 '''Returns True if intra-site topology is disabled for site'''
1724 return (self.site_options &
1725 dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0
1727 def is_intersite_topology_disabled(self):
1728 '''Returns True if inter-site topology is disabled for site'''
1729 return ((self.site_options &
1730 dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED)
1731 != 0)
1733 def is_random_bridgehead_disabled(self):
1734 '''Returns True if selection of random bridgehead is disabled'''
1735 return (self.site_options &
1736 dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0
1738 def is_detect_stale_disabled(self):
1739 '''Returns True if detect stale is disabled for site'''
1740 return (self.site_options &
1741 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0
1743 def is_cleanup_ntdsconn_disabled(self):
1744 '''Returns True if NTDS Connection cleanup is disabled for site'''
1745 return (self.site_options &
1746 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0
1748 def same_site(self, dsa):
1749 '''Return True if dsa is in this site'''
1750 if self.get_dsa(dsa.dsa_dnstr):
1751 return True
1752 return False
1754 def is_rodc_site(self):
1755 if len(self.dsa_table) > 0 and len(self.rw_dsa_table) == 0:
1756 return True
1757 return False
1759 def __str__(self):
1760 '''Debug dump string output of class'''
1761 text = "%s:" % self.__class__.__name__
1762 text = text + "\n\tdn=%s" % self.site_dnstr
1763 text = text + "\n\toptions=0x%X" % self.site_options
1764 text = text + "\n\ttopo_generator=%s" % self.site_topo_generator
1765 text = text + "\n\ttopo_failover=%d" % self.site_topo_failover
1766 for key, dsa in self.dsa_table.items():
1767 text = text + "\n%s" % dsa
1768 return text
1771 class GraphNode(object):
1772 """A graph node describing a set of edges that should be directed to it.
1774 Each edge is a connection for a particular naming context replica directed
1775 from another node in the forest to this node.
1778 def __init__(self, dsa_dnstr, max_node_edges):
1779 """Instantiate the graph node according to a DSA dn string
1781 :param max_node_edges: maximum number of edges that should ever
1782 be directed to the node
1784 self.max_edges = max_node_edges
1785 self.dsa_dnstr = dsa_dnstr
1786 self.edge_from = []
1788 def __str__(self):
1789 text = "%s:" % self.__class__.__name__
1790 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
1791 text = text + "\n\tmax_edges=%d" % self.max_edges
1793 for i, edge in enumerate(self.edge_from):
1794 text = text + "\n\tedge_from[%d]=%s" % (i, edge)
1795 return text
1797 def add_edge_from(self, from_dsa_dnstr):
1798 """Add an edge from the dsa to our graph nodes edge from list
1800 :param from_dsa_dnstr: the dsa that the edge emanates from
1802 assert from_dsa_dnstr is not None
1804 # No edges from myself to myself
1805 if from_dsa_dnstr == self.dsa_dnstr:
1806 return False
1807 # Only one edge from a particular node
1808 if from_dsa_dnstr in self.edge_from:
1809 return False
1810 # Not too many edges
1811 if len(self.edge_from) >= self.max_edges:
1812 return False
1813 self.edge_from.append(from_dsa_dnstr)
1814 return True
1816 def add_edges_from_connections(self, dsa):
1817 """For each nTDSConnection object associated with a particular
1818 DSA, we test if it implies an edge to this graph node (i.e.
1819 the "fromServer" attribute). If it does then we add an
1820 edge from the server unless we are over the max edges for this
1821 graph node
1823 :param dsa: dsa with a dnstr equivalent to his graph node
1825 for connect in dsa.connect_table.values():
1826 self.add_edge_from(connect.from_dnstr)
1828 def add_connections_from_edges(self, dsa, transport):
1829 """For each edge directed to this graph node, ensure there
1830 is a corresponding nTDSConnection object in the dsa.
1832 for edge_dnstr in self.edge_from:
1833 connections = dsa.get_connection_by_from_dnstr(edge_dnstr)
1835 # For each edge directed to the NC replica that
1836 # "should be present" on the local DC, the KCC determines
1837 # whether an object c exists such that:
1839 # c is a child of the DC's nTDSDSA object.
1840 # c.objectCategory = nTDSConnection
1842 # Given the NC replica ri from which the edge is directed,
1843 # c.fromServer is the dsname of the nTDSDSA object of
1844 # the DC on which ri "is present".
1846 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1848 found_valid = False
1849 for connect in connections:
1850 if connect.is_rodc_topology():
1851 continue
1852 found_valid = True
1854 if found_valid:
1855 continue
1857 # if no such object exists then the KCC adds an object
1858 # c with the following attributes
1860 # Generate a new dnstr for this nTDSConnection
1861 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1862 flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1863 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1865 dsa.new_connection(opt, flags, transport, edge_dnstr, None)
1867 def has_sufficient_edges(self):
1868 '''Return True if we have met the maximum "from edges" criteria'''
1869 if len(self.edge_from) >= self.max_edges:
1870 return True
1871 return False
1874 class Transport(object):
1875 """Class defines a Inter-site transport found under Sites
1878 def __init__(self, dnstr):
1879 self.dnstr = dnstr
1880 self.options = 0
1881 self.guid = None
1882 self.name = None
1883 self.address_attr = None
1884 self.bridgehead_list = []
1886 def __str__(self):
1887 '''Debug dump string output of Transport object'''
1889 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
1890 text = text + "\n\tguid=%s" % str(self.guid)
1891 text = text + "\n\toptions=%d" % self.options
1892 text = text + "\n\taddress_attr=%s" % self.address_attr
1893 text = text + "\n\tname=%s" % self.name
1894 for dnstr in self.bridgehead_list:
1895 text = text + "\n\tbridgehead_list=%s" % dnstr
1897 return text
1899 def load_transport(self, samdb):
1900 """Given a Transport object with an prior initialization
1901 for the object's DN, search for the DN and load attributes
1902 from the samdb.
1904 attrs = ["objectGUID",
1905 "options",
1906 "name",
1907 "bridgeheadServerListBL",
1908 "transportAddressAttribute"]
1909 try:
1910 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
1911 attrs=attrs)
1913 except ldb.LdbError, (enum, estr):
1914 raise KCCError("Unable to find Transport for (%s) - (%s)" %
1915 (self.dnstr, estr))
1917 msg = res[0]
1918 self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
1919 msg["objectGUID"][0]))
1921 if "options" in msg:
1922 self.options = int(msg["options"][0])
1924 if "transportAddressAttribute" in msg:
1925 self.address_attr = str(msg["transportAddressAttribute"][0])
1927 if "name" in msg:
1928 self.name = str(msg["name"][0])
1930 if "bridgeheadServerListBL" in msg:
1931 for value in msg["bridgeheadServerListBL"]:
1932 dsdn = dsdb_Dn(samdb, value)
1933 dnstr = str(dsdn.dn)
1934 if dnstr not in self.bridgehead_list:
1935 self.bridgehead_list.append(dnstr)
1938 class RepsFromTo(object):
1939 """Class encapsulation of the NDR repsFromToBlob.
1941 Removes the necessity of external code having to
1942 understand about other_info or manipulation of
1943 update flags.
1945 def __init__(self, nc_dnstr=None, ndr_blob=None):
1947 self.__dict__['to_be_deleted'] = False
1948 self.__dict__['nc_dnstr'] = nc_dnstr
1949 self.__dict__['update_flags'] = 0x0
1950 # XXX the following sounds dubious and/or better solved
1951 # elsewhere, but lets leave it for now. In particular, there
1952 # seems to be no reason for all the non-ndr generated
1953 # attributes to be handled in the round about way (e.g.
1954 # self.__dict__['to_be_deleted'] = False above). On the other
1955 # hand, it all seems to work. Hooray! Hands off!.
1957 # WARNING:
1959 # There is a very subtle bug here with python
1960 # and our NDR code. If you assign directly to
1961 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1962 # then a proper python GC reference count is not
1963 # maintained.
1965 # To work around this we maintain an internal
1966 # reference to "dns_name(x)" and "other_info" elements
1967 # of repsFromToBlob. This internal reference
1968 # is hidden within this class but it is why you
1969 # see statements like this below:
1971 # self.__dict__['ndr_blob'].ctr.other_info = \
1972 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1974 # That would appear to be a redundant assignment but
1975 # it is necessary to hold a proper python GC reference
1976 # count.
1977 if ndr_blob is None:
1978 self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
1979 self.__dict__['ndr_blob'].version = 0x1
1980 self.__dict__['dns_name1'] = None
1981 self.__dict__['dns_name2'] = None
1983 self.__dict__['ndr_blob'].ctr.other_info = \
1984 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1986 else:
1987 self.__dict__['ndr_blob'] = ndr_blob
1988 self.__dict__['other_info'] = ndr_blob.ctr.other_info
1990 if ndr_blob.version == 0x1:
1991 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
1992 self.__dict__['dns_name2'] = None
1993 else:
1994 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
1995 self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
1997 def __str__(self):
1998 '''Debug dump string output of class'''
2000 text = "%s:" % self.__class__.__name__
2001 text += "\n\tdnstr=%s" % self.nc_dnstr
2002 text += "\n\tupdate_flags=0x%X" % self.update_flags
2003 text += "\n\tversion=%d" % self.version
2004 text += "\n\tsource_dsa_obj_guid=%s" % self.source_dsa_obj_guid
2005 text += ("\n\tsource_dsa_invocation_id=%s" %
2006 self.source_dsa_invocation_id)
2007 text += "\n\ttransport_guid=%s" % self.transport_guid
2008 text += "\n\treplica_flags=0x%X" % self.replica_flags
2009 text += ("\n\tconsecutive_sync_failures=%d" %
2010 self.consecutive_sync_failures)
2011 text += "\n\tlast_success=%s" % self.last_success
2012 text += "\n\tlast_attempt=%s" % self.last_attempt
2013 text += "\n\tdns_name1=%s" % self.dns_name1
2014 text += "\n\tdns_name2=%s" % self.dns_name2
2015 text += "\n\tschedule[ "
2016 for slot in self.schedule:
2017 text += "0x%X " % slot
2018 text += "]"
2020 return text
2022 def __setattr__(self, item, value):
2023 """Set an attribute and chyange update flag.
2025 Be aware that setting any RepsFromTo attribute will set the
2026 drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS update flag.
2028 if item in ['schedule', 'replica_flags', 'transport_guid',
2029 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2030 'consecutive_sync_failures', 'last_success',
2031 'last_attempt']:
2033 if item in ['replica_flags']:
2034 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
2035 elif item in ['schedule']:
2036 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
2038 setattr(self.__dict__['ndr_blob'].ctr, item, value)
2040 elif item in ['dns_name1']:
2041 self.__dict__['dns_name1'] = value
2043 if self.__dict__['ndr_blob'].version == 0x1:
2044 self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
2045 self.__dict__['dns_name1']
2046 else:
2047 self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
2048 self.__dict__['dns_name1']
2050 elif item in ['dns_name2']:
2051 self.__dict__['dns_name2'] = value
2053 if self.__dict__['ndr_blob'].version == 0x1:
2054 raise AttributeError(item)
2055 else:
2056 self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
2057 self.__dict__['dns_name2']
2059 elif item in ['nc_dnstr']:
2060 self.__dict__['nc_dnstr'] = value
2062 elif item in ['to_be_deleted']:
2063 self.__dict__['to_be_deleted'] = value
2065 elif item in ['version']:
2066 raise AttributeError("Attempt to set readonly attribute %s" % item)
2067 else:
2068 raise AttributeError("Unknown attribute %s" % item)
2070 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
2072 def __getattr__(self, item):
2073 """Overload of RepsFromTo attribute retrieval.
2075 Allows external code to ignore substructures within the blob
2077 if item in ['schedule', 'replica_flags', 'transport_guid',
2078 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2079 'consecutive_sync_failures', 'last_success',
2080 'last_attempt']:
2081 return getattr(self.__dict__['ndr_blob'].ctr, item)
2083 elif item in ['version']:
2084 return self.__dict__['ndr_blob'].version
2086 elif item in ['dns_name1']:
2087 if self.__dict__['ndr_blob'].version == 0x1:
2088 return self.__dict__['ndr_blob'].ctr.other_info.dns_name
2089 else:
2090 return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
2092 elif item in ['dns_name2']:
2093 if self.__dict__['ndr_blob'].version == 0x1:
2094 raise AttributeError(item)
2095 else:
2096 return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
2098 elif item in ['to_be_deleted']:
2099 return self.__dict__['to_be_deleted']
2101 elif item in ['nc_dnstr']:
2102 return self.__dict__['nc_dnstr']
2104 elif item in ['update_flags']:
2105 return self.__dict__['update_flags']
2107 raise AttributeError("Unknown attribute %s" % item)
2109 def is_modified(self):
2110 return (self.update_flags != 0x0)
2112 def set_unmodified(self):
2113 self.__dict__['update_flags'] = 0x0
2116 class SiteLink(object):
2117 """Class defines a site link found under sites
2120 def __init__(self, dnstr):
2121 self.dnstr = dnstr
2122 self.options = 0
2123 self.system_flags = 0
2124 self.cost = 0
2125 self.schedule = None
2126 self.interval = None
2127 self.site_list = []
2129 def __str__(self):
2130 '''Debug dump string output of Transport object'''
2132 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
2133 text = text + "\n\toptions=%d" % self.options
2134 text = text + "\n\tsystem_flags=%d" % self.system_flags
2135 text = text + "\n\tcost=%d" % self.cost
2136 text = text + "\n\tinterval=%s" % self.interval
2138 if self.schedule is not None:
2139 text += "\n\tschedule.size=%s" % self.schedule.size
2140 text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
2141 text += ("\n\tschedule.numberOfSchedules=%s" %
2142 self.schedule.numberOfSchedules)
2144 for i, header in enumerate(self.schedule.headerArray):
2145 text += ("\n\tschedule.headerArray[%d].type=%d" %
2146 (i, header.type))
2147 text += ("\n\tschedule.headerArray[%d].offset=%d" %
2148 (i, header.offset))
2149 text = text + "\n\tschedule.dataArray[%d].slots[ " % i
2150 for slot in self.schedule.dataArray[i].slots:
2151 text = text + "0x%X " % slot
2152 text = text + "]"
2154 for dnstr in self.site_list:
2155 text = text + "\n\tsite_list=%s" % dnstr
2156 return text
2158 def load_sitelink(self, samdb):
2159 """Given a siteLink object with an prior initialization
2160 for the object's DN, search for the DN and load attributes
2161 from the samdb.
2163 attrs = ["options",
2164 "systemFlags",
2165 "cost",
2166 "schedule",
2167 "replInterval",
2168 "siteList"]
2169 try:
2170 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
2171 attrs=attrs, controls=['extended_dn:0'])
2173 except ldb.LdbError, (enum, estr):
2174 raise KCCError("Unable to find SiteLink for (%s) - (%s)" %
2175 (self.dnstr, estr))
2177 msg = res[0]
2179 if "options" in msg:
2180 self.options = int(msg["options"][0])
2182 if "systemFlags" in msg:
2183 self.system_flags = int(msg["systemFlags"][0])
2185 if "cost" in msg:
2186 self.cost = int(msg["cost"][0])
2188 if "replInterval" in msg:
2189 self.interval = int(msg["replInterval"][0])
2191 if "siteList" in msg:
2192 for value in msg["siteList"]:
2193 dsdn = dsdb_Dn(samdb, value)
2194 guid = misc.GUID(dsdn.dn.get_extended_component('GUID'))
2195 if guid not in self.site_list:
2196 self.site_list.append(guid)
2198 if "schedule" in msg:
2199 self.schedule = ndr_unpack(drsblobs.schedule, value)
2200 else:
2201 self.schedule = new_connection_schedule()
2204 class KCCFailedObject(object):
2205 def __init__(self, uuid, failure_count, time_first_failure,
2206 last_result, dns_name):
2207 self.uuid = uuid
2208 self.failure_count = failure_count
2209 self.time_first_failure = time_first_failure
2210 self.last_result = last_result
2211 self.dns_name = dns_name
2214 ##################################################
2215 # Global Functions and Variables
2216 ##################################################
2218 def get_dsa_config_rep(dsa):
2219 # Find configuration NC replica for the DSA
2220 for c_rep in dsa.current_rep_table.values():
2221 if c_rep.is_config():
2222 return c_rep
2224 raise KCCError("Unable to find config NC replica for (%s)" %
2225 dsa.dsa_dnstr)
2228 def sort_dsa_by_guid(dsa1, dsa2):
2229 "use ndr_pack for GUID comparison, as appears correct in some places"""
2230 return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
2233 def new_connection_schedule():
2234 """Create a default schedule for an NTDSConnection or Sitelink. This
2235 is packed differently from the repltimes schedule used elsewhere
2236 in KCC (where the 168 nibbles are packed into 84 bytes).
2238 # 168 byte instances of the 0x01 value. The low order 4 bits
2239 # of the byte equate to 15 minute intervals within a single hour.
2240 # There are 168 bytes because there are 168 hours in a full week
2241 # Effectively we are saying to perform replication at the end of
2242 # each hour of the week
2243 schedule = drsblobs.schedule()
2245 schedule.size = 188
2246 schedule.bandwidth = 0
2247 schedule.numberOfSchedules = 1
2249 header = drsblobs.scheduleHeader()
2250 header.type = 0
2251 header.offset = 20
2253 schedule.headerArray = [header]
2255 data = drsblobs.scheduleSlots()
2256 data.slots = [0x01] * 168
2258 schedule.dataArray = [data]
2259 return schedule