smbd: Make SMB3 clients use encryption with "smb encrypt = auto"
[Samba.git] / python / samba / kcc_utils.py
blob9c6b76251b629277cbb7aa6906c7d49796dcb455
1 # KCC topology utilities
3 # Copyright (C) Dave Craft 2011
4 # Copyright (C) Jelmer Vernooij 2011
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import ldb
20 import uuid
21 import time
23 from samba import dsdb, unix2nttime
24 from samba.dcerpc import (
25 drsblobs,
26 drsuapi,
27 misc,
29 from samba.common import dsdb_Dn
30 from samba.ndr import (ndr_unpack, ndr_pack)
33 class NCType(object):
34 (unknown, schema, domain, config, application) = range(0, 5)
37 class NamingContext(object):
38 """Base class for a naming context.
40 Holds the DN, GUID, SID (if available) and type of the DN.
41 Subclasses may inherit from this and specialize
42 """
44 def __init__(self, nc_dnstr):
45 """Instantiate a NamingContext
47 :param nc_dnstr: NC dn string
48 """
49 self.nc_dnstr = nc_dnstr
50 self.nc_guid = None
51 self.nc_sid = None
52 self.nc_type = NCType.unknown
54 def __str__(self):
55 '''Debug dump string output of class'''
56 text = "%s:" % self.__class__.__name__
57 text = text + "\n\tnc_dnstr=%s" % self.nc_dnstr
58 text = text + "\n\tnc_guid=%s" % str(self.nc_guid)
60 if self.nc_sid is None:
61 text = text + "\n\tnc_sid=<absent>"
62 else:
63 text = text + "\n\tnc_sid=<present>"
65 text = text + "\n\tnc_type=%s" % self.nc_type
66 return text
68 def load_nc(self, samdb):
69 attrs = [ "objectGUID",
70 "objectSid" ]
71 try:
72 res = samdb.search(base=self.nc_dnstr,
73 scope=ldb.SCOPE_BASE, attrs=attrs)
75 except ldb.LdbError, (enum, estr):
76 raise Exception("Unable to find naming context (%s)" %
77 (self.nc_dnstr, estr))
78 msg = res[0]
79 if "objectGUID" in msg:
80 self.nc_guid = misc.GUID(samdb.schema_format_value("objectGUID",
81 msg["objectGUID"][0]))
82 if "objectSid" in msg:
83 self.nc_sid = msg["objectSid"][0]
85 assert self.nc_guid is not None
87 def is_schema(self):
88 '''Return True if NC is schema'''
89 assert self.nc_type != NCType.unknown
90 return self.nc_type == NCType.schema
92 def is_domain(self):
93 '''Return True if NC is domain'''
94 assert self.nc_type != NCType.unknown
95 return self.nc_type == NCType.domain
97 def is_application(self):
98 '''Return True if NC is application'''
99 assert self.nc_type != NCType.unknown
100 return self.nc_type == NCType.application
102 def is_config(self):
103 '''Return True if NC is config'''
104 assert self.nc_type != NCType.unknown
105 return self.nc_type == NCType.config
107 def identify_by_basedn(self, samdb):
108 """Given an NC object, identify what type is is thru
109 the samdb basedn strings and NC sid value
111 # Invoke loader to initialize guid and more
112 # importantly sid value (sid is used to identify
113 # domain NCs)
114 if self.nc_guid is None:
115 self.load_nc(samdb)
117 # We check against schema and config because they
118 # will be the same for all nTDSDSAs in the forest.
119 # That leaves the domain NCs which can be identified
120 # by sid and application NCs as the last identified
121 if self.nc_dnstr == str(samdb.get_schema_basedn()):
122 self.nc_type = NCType.schema
123 elif self.nc_dnstr == str(samdb.get_config_basedn()):
124 self.nc_type = NCType.config
125 elif self.nc_sid is not None:
126 self.nc_type = NCType.domain
127 else:
128 self.nc_type = NCType.application
130 def identify_by_dsa_attr(self, samdb, attr):
131 """Given an NC which has been discovered thru the
132 nTDSDSA database object, determine what type of NC
133 it is (i.e. schema, config, domain, application) via
134 the use of the schema attribute under which the NC
135 was found.
137 :param attr: attr of nTDSDSA object where NC DN appears
139 # If the NC is listed under msDS-HasDomainNCs then
140 # this can only be a domain NC and it is our default
141 # domain for this dsa
142 if attr == "msDS-HasDomainNCs":
143 self.nc_type = NCType.domain
145 # If the NC is listed under hasPartialReplicaNCs
146 # this is only a domain NC
147 elif attr == "hasPartialReplicaNCs":
148 self.nc_type = NCType.domain
150 # NCs listed under hasMasterNCs are either
151 # default domain, schema, or config. We
152 # utilize the identify_by_basedn() to
153 # identify those
154 elif attr == "hasMasterNCs":
155 self.identify_by_basedn(samdb)
157 # Still unknown (unlikely) but for completeness
158 # and for finally identifying application NCs
159 if self.nc_type == NCType.unknown:
160 self.identify_by_basedn(samdb)
163 class NCReplica(NamingContext):
164 """Naming context replica that is relative to a specific DSA.
166 This is a more specific form of NamingContext class (inheriting from that
167 class) and it identifies unique attributes of the DSA's replica for a NC.
170 def __init__(self, dsa_dnstr, dsa_guid, nc_dnstr):
171 """Instantiate a Naming Context Replica
173 :param dsa_guid: GUID of DSA where replica appears
174 :param nc_dnstr: NC dn string
176 self.rep_dsa_dnstr = dsa_dnstr
177 self.rep_dsa_guid = dsa_guid
178 self.rep_default = False # replica for DSA's default domain
179 self.rep_partial = False
180 self.rep_ro = False
181 self.rep_instantiated_flags = 0
183 self.rep_fsmo_role_owner = None
185 # RepsFromTo tuples
186 self.rep_repsFrom = []
188 # The (is present) test is a combination of being
189 # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or
190 # hasPartialReplicaNCs) as well as its replica flags found
191 # thru the msDS-HasInstantiatedNCs. If the NC replica meets
192 # the first enumeration test then this flag is set true
193 self.rep_present_criteria_one = False
195 # Call my super class we inherited from
196 NamingContext.__init__(self, nc_dnstr)
198 def __str__(self):
199 '''Debug dump string output of class'''
200 text = "%s:" % self.__class__.__name__
201 text = text + "\n\tdsa_dnstr=%s" % self.rep_dsa_dnstr
202 text = text + "\n\tdsa_guid=%s" % str(self.rep_dsa_guid)
203 text = text + "\n\tdefault=%s" % self.rep_default
204 text = text + "\n\tro=%s" % self.rep_ro
205 text = text + "\n\tpartial=%s" % self.rep_partial
206 text = text + "\n\tpresent=%s" % self.is_present()
207 text = text + "\n\tfsmo_role_owner=%s" % self.rep_fsmo_role_owner
209 for rep in self.rep_repsFrom:
210 text = text + "\n%s" % rep
212 return "%s\n%s" % (NamingContext.__str__(self), text)
214 def set_instantiated_flags(self, flags=None):
215 '''Set or clear NC replica instantiated flags'''
216 if flags is None:
217 self.rep_instantiated_flags = 0
218 else:
219 self.rep_instantiated_flags = flags
221 def identify_by_dsa_attr(self, samdb, attr):
222 """Given an NC which has been discovered thru the
223 nTDSDSA database object, determine what type of NC
224 replica it is (i.e. partial, read only, default)
226 :param attr: attr of nTDSDSA object where NC DN appears
228 # If the NC was found under hasPartialReplicaNCs
229 # then a partial replica at this dsa
230 if attr == "hasPartialReplicaNCs":
231 self.rep_partial = True
232 self.rep_present_criteria_one = True
234 # If the NC is listed under msDS-HasDomainNCs then
235 # this can only be a domain NC and it is the DSA's
236 # default domain NC
237 elif attr == "msDS-HasDomainNCs":
238 self.rep_default = True
240 # NCs listed under hasMasterNCs are either
241 # default domain, schema, or config. We check
242 # against schema and config because they will be
243 # the same for all nTDSDSAs in the forest. That
244 # leaves the default domain NC remaining which
245 # may be different for each nTDSDSAs (and thus
246 # we don't compare agains this samdb's default
247 # basedn
248 elif attr == "hasMasterNCs":
249 self.rep_present_criteria_one = True
251 if self.nc_dnstr != str(samdb.get_schema_basedn()) and \
252 self.nc_dnstr != str(samdb.get_config_basedn()):
253 self.rep_default = True
255 # RODC only
256 elif attr == "msDS-hasFullReplicaNCs":
257 self.rep_present_criteria_one = True
258 self.rep_ro = True
260 # Not RODC
261 elif attr == "msDS-hasMasterNCs":
262 self.rep_ro = False
264 # Now use this DSA attribute to identify the naming
265 # context type by calling the super class method
266 # of the same name
267 NamingContext.identify_by_dsa_attr(self, samdb, attr)
269 def is_default(self):
270 """Whether this is a default domain for the dsa that this NC appears on
272 return self.rep_default
274 def is_ro(self):
275 '''Return True if NC replica is read only'''
276 return self.rep_ro
278 def is_partial(self):
279 '''Return True if NC replica is partial'''
280 return self.rep_partial
282 def is_present(self):
283 """Given an NC replica which has been discovered thru the
284 nTDSDSA database object and populated with replica flags
285 from the msDS-HasInstantiatedNCs; return whether the NC
286 replica is present (true) or if the IT_NC_GOING flag is
287 set then the NC replica is not present (false)
289 if self.rep_present_criteria_one and \
290 self.rep_instantiated_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0:
291 return True
292 return False
294 def load_repsFrom(self, samdb):
295 """Given an NC replica which has been discovered thru the nTDSDSA
296 database object, load the repsFrom attribute for the local replica.
297 held by my dsa. The repsFrom attribute is not replicated so this
298 attribute is relative only to the local DSA that the samdb exists on
300 try:
301 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
302 attrs=[ "repsFrom" ])
304 except ldb.LdbError, (enum, estr):
305 raise Exception("Unable to find NC for (%s) - (%s)" %
306 (self.nc_dnstr, estr))
308 msg = res[0]
310 # Possibly no repsFrom if this is a singleton DC
311 if "repsFrom" in msg:
312 for value in msg["repsFrom"]:
313 rep = RepsFromTo(self.nc_dnstr,
314 ndr_unpack(drsblobs.repsFromToBlob, value))
315 self.rep_repsFrom.append(rep)
317 def commit_repsFrom(self, samdb, ro=False):
318 """Commit repsFrom to the database"""
320 # XXX - This is not truly correct according to the MS-TECH
321 # docs. To commit a repsFrom we should be using RPCs
322 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
323 # IDL_DRSReplicaDel to affect a repsFrom change.
325 # Those RPCs are missing in samba, so I'll have to
326 # implement them to get this to more accurately
327 # reflect the reference docs. As of right now this
328 # commit to the database will work as its what the
329 # older KCC also did
330 modify = False
331 newreps = []
332 delreps = []
334 for repsFrom in self.rep_repsFrom:
336 # Leave out any to be deleted from
337 # replacement list. Build a list
338 # of to be deleted reps which we will
339 # remove from rep_repsFrom list below
340 if repsFrom.to_be_deleted:
341 delreps.append(repsFrom)
342 modify = True
343 continue
345 if repsFrom.is_modified():
346 repsFrom.set_unmodified()
347 modify = True
349 # current (unmodified) elements also get
350 # appended here but no changes will occur
351 # unless something is "to be modified" or
352 # "to be deleted"
353 newreps.append(ndr_pack(repsFrom.ndr_blob))
355 # Now delete these from our list of rep_repsFrom
356 for repsFrom in delreps:
357 self.rep_repsFrom.remove(repsFrom)
358 delreps = []
360 # Nothing to do if no reps have been modified or
361 # need to be deleted or input option has informed
362 # us to be "readonly" (ro). Leave database
363 # record "as is"
364 if not modify or ro:
365 return
367 m = ldb.Message()
368 m.dn = ldb.Dn(samdb, self.nc_dnstr)
370 m["repsFrom"] = \
371 ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsFrom")
373 try:
374 samdb.modify(m)
376 except ldb.LdbError, estr:
377 raise Exception("Could not set repsFrom for (%s) - (%s)" %
378 (self.dsa_dnstr, estr))
380 def dumpstr_to_be_deleted(self):
381 text=""
382 for repsFrom in self.rep_repsFrom:
383 if repsFrom.to_be_deleted:
384 if text:
385 text = text + "\n%s" % repsFrom
386 else:
387 text = "%s" % repsFrom
388 return text
390 def dumpstr_to_be_modified(self):
391 text=""
392 for repsFrom in self.rep_repsFrom:
393 if repsFrom.is_modified():
394 if text:
395 text = text + "\n%s" % repsFrom
396 else:
397 text = "%s" % repsFrom
398 return text
400 def load_fsmo_roles(self, samdb):
401 """Given an NC replica which has been discovered thru the nTDSDSA
402 database object, load the fSMORoleOwner attribute.
404 try:
405 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
406 attrs=[ "fSMORoleOwner" ])
408 except ldb.LdbError, (enum, estr):
409 raise Exception("Unable to find NC for (%s) - (%s)" %
410 (self.nc_dnstr, estr))
412 msg = res[0]
414 # Possibly no fSMORoleOwner
415 if "fSMORoleOwner" in msg:
416 self.rep_fsmo_role_owner = msg["fSMORoleOwner"]
418 def is_fsmo_role_owner(self, dsa_dnstr):
419 if self.rep_fsmo_role_owner is not None and \
420 self.rep_fsmo_role_owner == dsa_dnstr:
421 return True
422 return False
425 class DirectoryServiceAgent(object):
427 def __init__(self, dsa_dnstr):
428 """Initialize DSA class.
430 Class is subsequently fully populated by calling the load_dsa() method
432 :param dsa_dnstr: DN of the nTDSDSA
434 self.dsa_dnstr = dsa_dnstr
435 self.dsa_guid = None
436 self.dsa_ivid = None
437 self.dsa_is_ro = False
438 self.dsa_is_istg = False
439 self.dsa_options = 0
440 self.dsa_behavior = 0
441 self.default_dnstr = None # default domain dn string for dsa
443 # NCReplicas for this dsa that are "present"
444 # Indexed by DN string of naming context
445 self.current_rep_table = {}
447 # NCReplicas for this dsa that "should be present"
448 # Indexed by DN string of naming context
449 self.needed_rep_table = {}
451 # NTDSConnections for this dsa. These are current
452 # valid connections that are committed or pending a commit
453 # in the database. Indexed by DN string of connection
454 self.connect_table = {}
456 def __str__(self):
457 '''Debug dump string output of class'''
459 text = "%s:" % self.__class__.__name__
460 if self.dsa_dnstr is not None:
461 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
462 if self.dsa_guid is not None:
463 text = text + "\n\tdsa_guid=%s" % str(self.dsa_guid)
464 if self.dsa_ivid is not None:
465 text = text + "\n\tdsa_ivid=%s" % str(self.dsa_ivid)
467 text = text + "\n\tro=%s" % self.is_ro()
468 text = text + "\n\tgc=%s" % self.is_gc()
469 text = text + "\n\tistg=%s" % self.is_istg()
471 text = text + "\ncurrent_replica_table:"
472 text = text + "\n%s" % self.dumpstr_current_replica_table()
473 text = text + "\nneeded_replica_table:"
474 text = text + "\n%s" % self.dumpstr_needed_replica_table()
475 text = text + "\nconnect_table:"
476 text = text + "\n%s" % self.dumpstr_connect_table()
478 return text
480 def get_current_replica(self, nc_dnstr):
481 if nc_dnstr in self.current_rep_table.keys():
482 return self.current_rep_table[nc_dnstr]
483 else:
484 return None
486 def is_istg(self):
487 '''Returns True if dsa is intersite topology generator for it's site'''
488 # The KCC on an RODC always acts as an ISTG for itself
489 return self.dsa_is_istg or self.dsa_is_ro
491 def is_ro(self):
492 '''Returns True if dsa a read only domain controller'''
493 return self.dsa_is_ro
495 def is_gc(self):
496 '''Returns True if dsa hosts a global catalog'''
497 if (self.options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
498 return True
499 return False
501 def is_minimum_behavior(self, version):
502 """Is dsa at minimum windows level greater than or equal to (version)
504 :param version: Windows version to test against
505 (e.g. DS_DOMAIN_FUNCTION_2008)
507 if self.dsa_behavior >= version:
508 return True
509 return False
511 def is_translate_ntdsconn_disabled(self):
512 """Whether this allows NTDSConnection translation in its options."""
513 if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0:
514 return True
515 return False
517 def get_rep_tables(self):
518 """Return DSA current and needed replica tables
520 return self.current_rep_table, self.needed_rep_table
522 def get_parent_dnstr(self):
523 """Get the parent DN string of this object."""
524 head, sep, tail = self.dsa_dnstr.partition(',')
525 return tail
527 def load_dsa(self, samdb):
528 """Load a DSA from the samdb.
530 Prior initialization has given us the DN of the DSA that we are to
531 load. This method initializes all other attributes, including loading
532 the NC replica table for this DSA.
534 attrs = ["objectGUID",
535 "invocationID",
536 "options",
537 "msDS-isRODC",
538 "msDS-Behavior-Version"]
539 try:
540 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
541 attrs=attrs)
543 except ldb.LdbError, (enum, estr):
544 raise Exception("Unable to find nTDSDSA for (%s) - (%s)" %
545 (self.dsa_dnstr, estr))
547 msg = res[0]
548 self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID",
549 msg["objectGUID"][0]))
551 # RODCs don't originate changes and thus have no invocationId,
552 # therefore we must check for existence first
553 if "invocationId" in msg:
554 self.dsa_ivid = misc.GUID(samdb.schema_format_value("objectGUID",
555 msg["invocationId"][0]))
557 if "options" in msg:
558 self.options = int(msg["options"][0])
560 if "msDS-isRODC" in msg and msg["msDS-isRODC"][0] == "TRUE":
561 self.dsa_is_ro = True
562 else:
563 self.dsa_is_ro = False
565 if "msDS-Behavior-Version" in msg:
566 self.dsa_behavior = int(msg['msDS-Behavior-Version'][0])
568 # Load the NC replicas that are enumerated on this dsa
569 self.load_current_replica_table(samdb)
571 # Load the nTDSConnection that are enumerated on this dsa
572 self.load_connection_table(samdb)
574 def load_current_replica_table(self, samdb):
575 """Method to load the NC replica's listed for DSA object.
577 This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs,
578 hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, and
579 msDS-HasInstantiatedNCs) to determine complete list of NC replicas that
580 are enumerated for the DSA. Once a NC replica is loaded it is
581 identified (schema, config, etc) and the other replica attributes
582 (partial, ro, etc) are determined.
584 :param samdb: database to query for DSA replica list
586 ncattrs = [ # not RODC - default, config, schema (old style)
587 "hasMasterNCs",
588 # not RODC - default, config, schema, app NCs
589 "msDS-hasMasterNCs",
590 # domain NC partial replicas
591 "hasPartialReplicaNCs",
592 # default domain NC
593 "msDS-HasDomainNCs",
594 # RODC only - default, config, schema, app NCs
595 "msDS-hasFullReplicaNCs",
596 # Identifies if replica is coming, going, or stable
597 "msDS-HasInstantiatedNCs" ]
598 try:
599 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
600 attrs=ncattrs)
602 except ldb.LdbError, (enum, estr):
603 raise Exception("Unable to find nTDSDSA NCs for (%s) - (%s)" %
604 (self.dsa_dnstr, estr))
606 # The table of NCs for the dsa we are searching
607 tmp_table = {}
609 # We should get one response to our query here for
610 # the ntds that we requested
611 if len(res[0]) > 0:
613 # Our response will contain a number of elements including
614 # the dn of the dsa as well as elements for each
615 # attribute (e.g. hasMasterNCs). Each of these elements
616 # is a dictonary list which we retrieve the keys for and
617 # then iterate over them
618 for k in res[0].keys():
619 if k == "dn":
620 continue
622 # For each attribute type there will be one or more DNs
623 # listed. For instance DCs normally have 3 hasMasterNCs
624 # listed.
625 for value in res[0][k]:
626 # Turn dn into a dsdb_Dn so we can use
627 # its methods to parse a binary DN
628 dsdn = dsdb_Dn(samdb, value)
629 flags = dsdn.get_binary_integer()
630 dnstr = str(dsdn.dn)
632 if not dnstr in tmp_table.keys():
633 rep = NCReplica(self.dsa_dnstr, self.dsa_guid, dnstr)
634 tmp_table[dnstr] = rep
635 else:
636 rep = tmp_table[dnstr]
638 if k == "msDS-HasInstantiatedNCs":
639 rep.set_instantiated_flags(flags)
640 continue
642 rep.identify_by_dsa_attr(samdb, k)
644 # if we've identified the default domain NC
645 # then save its DN string
646 if rep.is_default():
647 self.default_dnstr = dnstr
648 else:
649 raise Exception("No nTDSDSA NCs for (%s)" % self.dsa_dnstr)
651 # Assign our newly built NC replica table to this dsa
652 self.current_rep_table = tmp_table
654 def add_needed_replica(self, rep):
655 """Method to add a NC replica that "should be present" to the
656 needed_rep_table if not already in the table
658 if not rep.nc_dnstr in self.needed_rep_table.keys():
659 self.needed_rep_table[rep.nc_dnstr] = rep
661 def load_connection_table(self, samdb):
662 """Method to load the nTDSConnections listed for DSA object.
664 :param samdb: database to query for DSA connection list
666 try:
667 res = samdb.search(base=self.dsa_dnstr,
668 scope=ldb.SCOPE_SUBTREE,
669 expression="(objectClass=nTDSConnection)")
671 except ldb.LdbError, (enum, estr):
672 raise Exception("Unable to find nTDSConnection for (%s) - (%s)" %
673 (self.dsa_dnstr, estr))
675 for msg in res:
676 dnstr = str(msg.dn)
678 # already loaded
679 if dnstr in self.connect_table.keys():
680 continue
682 connect = NTDSConnection(dnstr)
684 connect.load_connection(samdb)
685 self.connect_table[dnstr] = connect
687 def commit_connections(self, samdb, ro=False):
688 """Method to commit any uncommitted nTDSConnections
689 modifications that are in our table. These would be
690 identified connections that are marked to be added or
691 deleted
693 :param samdb: database to commit DSA connection list to
694 :param ro: if (true) then peform internal operations but
695 do not write to the database (readonly)
697 delconn = []
699 for dnstr, connect in self.connect_table.items():
700 if connect.to_be_added:
701 connect.commit_added(samdb, ro)
703 if connect.to_be_modified:
704 connect.commit_modified(samdb, ro)
706 if connect.to_be_deleted:
707 connect.commit_deleted(samdb, ro)
708 delconn.append(dnstr)
710 # Now delete the connection from the table
711 for dnstr in delconn:
712 del self.connect_table[dnstr]
714 def add_connection(self, dnstr, connect):
715 assert dnstr not in self.connect_table.keys()
716 self.connect_table[dnstr] = connect
718 def get_connection_by_from_dnstr(self, from_dnstr):
719 """Scan DSA nTDSConnection table and return connection
720 with a "fromServer" dn string equivalent to method
721 input parameter.
723 :param from_dnstr: search for this from server entry
725 for dnstr, connect in self.connect_table.items():
726 if connect.get_from_dnstr() == from_dnstr:
727 return connect
728 return None
730 def dumpstr_current_replica_table(self):
731 '''Debug dump string output of current replica table'''
732 text=""
733 for k in self.current_rep_table.keys():
734 if text:
735 text = text + "\n%s" % self.current_rep_table[k]
736 else:
737 text = "%s" % self.current_rep_table[k]
738 return text
740 def dumpstr_needed_replica_table(self):
741 '''Debug dump string output of needed replica table'''
742 text=""
743 for k in self.needed_rep_table.keys():
744 if text:
745 text = text + "\n%s" % self.needed_rep_table[k]
746 else:
747 text = "%s" % self.needed_rep_table[k]
748 return text
750 def dumpstr_connect_table(self):
751 '''Debug dump string output of connect table'''
752 text=""
753 for k in self.connect_table.keys():
754 if text:
755 text = text + "\n%s" % self.connect_table[k]
756 else:
757 text = "%s" % self.connect_table[k]
758 return text
760 def new_connection(self, options, flags, transport, from_dnstr, sched):
761 """Set up a new connection for the DSA based on input
762 parameters. Connection will be added to the DSA
763 connect_table and will be marked as "to be added" pending
764 a call to commit_connections()
766 dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
768 connect = NTDSConnection(dnstr)
769 connect.to_be_added = True
770 connect.enabled = True
771 connect.from_dnstr = from_dnstr
772 connect.options = options
773 connect.flags = flags
775 if transport is not None:
776 connect.transport_dnstr = transport.dnstr
778 if sched is not None:
779 connect.schedule = sched
780 else:
781 # Create schedule. Attribute valuse set according to MS-TECH
782 # intrasite connection creation document
783 connect.schedule = drsblobs.schedule()
785 connect.schedule.size = 188
786 connect.schedule.bandwidth = 0
787 connect.schedule.numberOfSchedules = 1
789 header = drsblobs.scheduleHeader()
790 header.type = 0
791 header.offset = 20
793 connect.schedule.headerArray = [ header ]
795 # 168 byte instances of the 0x01 value. The low order 4 bits
796 # of the byte equate to 15 minute intervals within a single hour.
797 # There are 168 bytes because there are 168 hours in a full week
798 # Effectively we are saying to perform replication at the end of
799 # each hour of the week
800 data = drsblobs.scheduleSlots()
801 data.slots = [ 0x01 ] * 168
803 connect.schedule.dataArray = [ data ]
805 self.add_connection(dnstr, connect);
806 return connect
809 class NTDSConnection(object):
810 """Class defines a nTDSConnection found under a DSA
812 def __init__(self, dnstr):
813 self.dnstr = dnstr
814 self.guid = None
815 self.enabled = False
816 self.whenCreated = 0
817 self.to_be_added = False # new connection needs to be added
818 self.to_be_deleted = False # old connection needs to be deleted
819 self.to_be_modified = False
820 self.options = 0
821 self.system_flags = 0
822 self.transport_dnstr = None
823 self.transport_guid = None
824 self.from_dnstr = None
825 self.schedule = None
827 def __str__(self):
828 '''Debug dump string output of NTDSConnection object'''
830 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
831 text = text + "\n\tenabled=%s" % self.enabled
832 text = text + "\n\tto_be_added=%s" % self.to_be_added
833 text = text + "\n\tto_be_deleted=%s" % self.to_be_deleted
834 text = text + "\n\tto_be_modified=%s" % self.to_be_modified
835 text = text + "\n\toptions=0x%08X" % self.options
836 text = text + "\n\tsystem_flags=0x%08X" % self.system_flags
837 text = text + "\n\twhenCreated=%d" % self.whenCreated
838 text = text + "\n\ttransport_dn=%s" % self.transport_dnstr
840 if self.guid is not None:
841 text = text + "\n\tguid=%s" % str(self.guid)
843 if self.transport_guid is not None:
844 text = text + "\n\ttransport_guid=%s" % str(self.transport_guid)
846 text = text + "\n\tfrom_dn=%s" % self.from_dnstr
848 if self.schedule is not None:
849 text = text + "\n\tschedule.size=%s" % self.schedule.size
850 text = text + "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
851 text = text + "\n\tschedule.numberOfSchedules=%s" % \
852 self.schedule.numberOfSchedules
854 for i, header in enumerate(self.schedule.headerArray):
855 text = text + "\n\tschedule.headerArray[%d].type=%d" % \
856 (i, header.type)
857 text = text + "\n\tschedule.headerArray[%d].offset=%d" % \
858 (i, header.offset)
859 text = text + "\n\tschedule.dataArray[%d].slots[ " % i
860 for slot in self.schedule.dataArray[i].slots:
861 text = text + "0x%X " % slot
862 text = text + "]"
864 return text
866 def load_connection(self, samdb):
867 """Given a NTDSConnection object with an prior initialization
868 for the object's DN, search for the DN and load attributes
869 from the samdb.
871 attrs = [ "options",
872 "enabledConnection",
873 "schedule",
874 "whenCreated",
875 "objectGUID",
876 "transportType",
877 "fromServer",
878 "systemFlags" ]
879 try:
880 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
881 attrs=attrs)
883 except ldb.LdbError, (enum, estr):
884 raise Exception("Unable to find nTDSConnection for (%s) - (%s)" %
885 (self.dnstr, estr))
887 msg = res[0]
889 if "options" in msg:
890 self.options = int(msg["options"][0])
892 if "enabledConnection" in msg:
893 if msg["enabledConnection"][0].upper().lstrip().rstrip() == "TRUE":
894 self.enabled = True
896 if "systemFlags" in msg:
897 self.system_flags = int(msg["systemFlags"][0])
899 if "objectGUID" in msg:
900 self.guid = \
901 misc.GUID(samdb.schema_format_value("objectGUID",
902 msg["objectGUID"][0]))
904 if "transportType" in msg:
905 dsdn = dsdb_Dn(samdb, msg["tranportType"][0])
906 self.load_connection_transport(samdb, str(dsdn.dn))
908 if "schedule" in msg:
909 self.schedule = ndr_unpack(drsblobs.replSchedule, msg["schedule"][0])
911 if "whenCreated" in msg:
912 self.whenCreated = ldb.string_to_time(msg["whenCreated"][0])
914 if "fromServer" in msg:
915 dsdn = dsdb_Dn(samdb, msg["fromServer"][0])
916 self.from_dnstr = str(dsdn.dn)
917 assert self.from_dnstr is not None
919 def load_connection_transport(self, samdb, tdnstr):
920 """Given a NTDSConnection object which enumerates a transport
921 DN, load the transport information for the connection object
923 :param tdnstr: transport DN to load
925 attrs = [ "objectGUID" ]
926 try:
927 res = samdb.search(base=tdnstr,
928 scope=ldb.SCOPE_BASE, attrs=attrs)
930 except ldb.LdbError, (enum, estr):
931 raise Exception("Unable to find transport (%s)" %
932 (tdnstr, estr))
934 if "objectGUID" in res[0]:
935 msg = res[0]
936 self.transport_dnstr = tdnstr
937 self.transport_guid = \
938 misc.GUID(samdb.schema_format_value("objectGUID",
939 msg["objectGUID"][0]))
940 assert self.transport_dnstr is not None
941 assert self.transport_guid is not None
943 def commit_deleted(self, samdb, ro=False):
944 """Local helper routine for commit_connections() which
945 handles committed connections that are to be deleted from
946 the database database
948 assert self.to_be_deleted
949 self.to_be_deleted = False
951 # No database modification requested
952 if ro:
953 return
955 try:
956 samdb.delete(self.dnstr)
957 except ldb.LdbError, (enum, estr):
958 raise Exception("Could not delete nTDSConnection for (%s) - (%s)" %
959 (self.dnstr, estr))
961 def commit_added(self, samdb, ro=False):
962 """Local helper routine for commit_connections() which
963 handles committed connections that are to be added to the
964 database
966 assert self.to_be_added
967 self.to_be_added = False
969 # No database modification requested
970 if ro:
971 return
973 # First verify we don't have this entry to ensure nothing
974 # is programatically amiss
975 found = False
976 try:
977 msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
978 if len(msg) != 0:
979 found = True
981 except ldb.LdbError, (enum, estr):
982 if enum != ldb.ERR_NO_SUCH_OBJECT:
983 raise Exception("Unable to search for (%s) - (%s)" %
984 (self.dnstr, estr))
985 if found:
986 raise Exception("nTDSConnection for (%s) already exists!" %
987 self.dnstr)
989 if self.enabled:
990 enablestr = "TRUE"
991 else:
992 enablestr = "FALSE"
994 # Prepare a message for adding to the samdb
995 m = ldb.Message()
996 m.dn = ldb.Dn(samdb, self.dnstr)
998 m["objectClass"] = \
999 ldb.MessageElement("nTDSConnection", ldb.FLAG_MOD_ADD,
1000 "objectClass")
1001 m["showInAdvancedViewOnly"] = \
1002 ldb.MessageElement("TRUE", ldb.FLAG_MOD_ADD,
1003 "showInAdvancedViewOnly")
1004 m["enabledConnection"] = \
1005 ldb.MessageElement(enablestr, ldb.FLAG_MOD_ADD, "enabledConnection")
1006 m["fromServer"] = \
1007 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_ADD, "fromServer")
1008 m["options"] = \
1009 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options")
1010 m["systemFlags"] = \
1011 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_ADD,
1012 "systemFlags")
1014 if self.transport_dnstr is not None:
1015 m["transportType"] = \
1016 ldb.MessageElement(str(self.transport_dnstr), ldb.FLAG_MOD_ADD,
1017 "transportType")
1019 if self.schedule is not None:
1020 m["schedule"] = \
1021 ldb.MessageElement(ndr_pack(self.schedule),
1022 ldb.FLAG_MOD_ADD, "schedule")
1023 try:
1024 samdb.add(m)
1025 except ldb.LdbError, (enum, estr):
1026 raise Exception("Could not add nTDSConnection for (%s) - (%s)" %
1027 (self.dnstr, estr))
1029 def commit_modified(self, samdb, ro=False):
1030 """Local helper routine for commit_connections() which
1031 handles committed connections that are to be modified to the
1032 database
1034 assert self.to_be_modified
1035 self.to_be_modified = False
1037 # No database modification requested
1038 if ro:
1039 return
1041 # First verify we have this entry to ensure nothing
1042 # is programatically amiss
1043 try:
1044 msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1045 found = True
1047 except ldb.LdbError, (enum, estr):
1048 if enum == ldb.ERR_NO_SUCH_OBJECT:
1049 found = False
1050 else:
1051 raise Exception("Unable to search for (%s) - (%s)" %
1052 (self.dnstr, estr))
1053 if not found:
1054 raise Exception("nTDSConnection for (%s) doesn't exist!" %
1055 self.dnstr)
1057 if self.enabled:
1058 enablestr = "TRUE"
1059 else:
1060 enablestr = "FALSE"
1062 # Prepare a message for modifying the samdb
1063 m = ldb.Message()
1064 m.dn = ldb.Dn(samdb, self.dnstr)
1066 m["enabledConnection"] = \
1067 ldb.MessageElement(enablestr, ldb.FLAG_MOD_REPLACE,
1068 "enabledConnection")
1069 m["fromServer"] = \
1070 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_REPLACE,
1071 "fromServer")
1072 m["options"] = \
1073 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_REPLACE,
1074 "options")
1075 m["systemFlags"] = \
1076 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_REPLACE,
1077 "systemFlags")
1079 if self.transport_dnstr is not None:
1080 m["transportType"] = \
1081 ldb.MessageElement(str(self.transport_dnstr),
1082 ldb.FLAG_MOD_REPLACE, "transportType")
1083 else:
1084 m["transportType"] = \
1085 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "transportType")
1087 if self.schedule is not None:
1088 m["schedule"] = \
1089 ldb.MessageElement(ndr_pack(self.schedule),
1090 ldb.FLAG_MOD_REPLACE, "schedule")
1091 else:
1092 m["schedule"] = \
1093 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "schedule")
1094 try:
1095 samdb.modify(m)
1096 except ldb.LdbError, (enum, estr):
1097 raise Exception("Could not modify nTDSConnection for (%s) - (%s)" %
1098 (self.dnstr, estr))
1100 def set_modified(self, truefalse):
1101 self.to_be_modified = truefalse
1103 def set_added(self, truefalse):
1104 self.to_be_added = truefalse
1106 def set_deleted(self, truefalse):
1107 self.to_be_deleted = truefalse
1109 def is_schedule_minimum_once_per_week(self):
1110 """Returns True if our schedule includes at least one
1111 replication interval within the week. False otherwise
1113 if self.schedule is None or self.schedule.dataArray[0] is None:
1114 return False
1116 for slot in self.schedule.dataArray[0].slots:
1117 if (slot & 0x0F) != 0x0:
1118 return True
1119 return False
1121 def is_equivalent_schedule(self, sched):
1122 """Returns True if our schedule is equivalent to the input
1123 comparison schedule.
1125 :param shed: schedule to compare to
1127 if self.schedule is not None:
1128 if sched is None:
1129 return False
1130 elif sched is None:
1131 return True
1133 if (self.schedule.size != sched.size or
1134 self.schedule.bandwidth != sched.bandwidth or
1135 self.schedule.numberOfSchedules != sched.numberOfSchedules):
1136 return False
1138 for i, header in enumerate(self.schedule.headerArray):
1140 if self.schedule.headerArray[i].type != sched.headerArray[i].type:
1141 return False
1143 if self.schedule.headerArray[i].offset != \
1144 sched.headerArray[i].offset:
1145 return False
1147 for a, b in zip(self.schedule.dataArray[i].slots,
1148 sched.dataArray[i].slots):
1149 if a != b:
1150 return False
1151 return True
1153 def convert_schedule_to_repltimes(self):
1154 """Convert NTDS Connection schedule to replTime schedule.
1156 NTDS Connection schedule slots are double the size of
1157 the replTime slots but the top portion of the NTDS
1158 Connection schedule slot (4 most significant bits in
1159 uchar) are unused. The 4 least significant bits have
1160 the same (15 minute interval) bit positions as replTimes.
1161 We thus pack two elements of the NTDS Connection schedule
1162 slots into one element of the replTimes slot
1163 If no schedule appears in NTDS Connection then a default
1164 of 0x11 is set in each replTimes slot as per behaviour
1165 noted in a Windows DC. That default would cause replication
1166 within the last 15 minutes of each hour.
1168 times = [0x11] * 84
1170 for i, slot in enumerate(times):
1171 if self.schedule is not None and \
1172 self.schedule.dataArray[0] is not None:
1173 slot = (self.schedule.dataArray[0].slots[i*2] & 0xF) << 4 | \
1174 (self.schedule.dataArray[0].slots[i*2] & 0xF)
1175 return times
1177 def is_rodc_topology(self):
1178 """Returns True if NTDS Connection specifies RODC
1179 topology only
1181 if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
1182 return False
1183 return True
1185 def is_generated(self):
1186 """Returns True if NTDS Connection was generated by the
1187 KCC topology algorithm as opposed to set by the administrator
1189 if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0:
1190 return False
1191 return True
1193 def is_override_notify_default(self):
1194 """Returns True if NTDS Connection should override notify default
1196 if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0:
1197 return False
1198 return True
1200 def is_use_notify(self):
1201 """Returns True if NTDS Connection should use notify
1203 if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0:
1204 return False
1205 return True
1207 def is_twoway_sync(self):
1208 """Returns True if NTDS Connection should use twoway sync
1210 if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0:
1211 return False
1212 return True
1214 def is_intersite_compression_disabled(self):
1215 """Returns True if NTDS Connection intersite compression
1216 is disabled
1218 if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0:
1219 return False
1220 return True
1222 def is_user_owned_schedule(self):
1223 """Returns True if NTDS Connection has a user owned schedule
1225 if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0:
1226 return False
1227 return True
1229 def is_enabled(self):
1230 """Returns True if NTDS Connection is enabled
1232 return self.enabled
1234 def get_from_dnstr(self):
1235 '''Return fromServer dn string attribute'''
1236 return self.from_dnstr
1239 class Partition(NamingContext):
1240 """A naming context discovered thru Partitions DN of the config schema.
1242 This is a more specific form of NamingContext class (inheriting from that
1243 class) and it identifies unique attributes enumerated in the Partitions
1244 such as which nTDSDSAs are cross referenced for replicas
1246 def __init__(self, partstr):
1247 self.partstr = partstr
1248 self.enabled = True
1249 self.system_flags = 0
1250 self.rw_location_list = []
1251 self.ro_location_list = []
1253 # We don't have enough info to properly
1254 # fill in the naming context yet. We'll get that
1255 # fully set up with load_partition().
1256 NamingContext.__init__(self, None)
1259 def load_partition(self, samdb):
1260 """Given a Partition class object that has been initialized with its
1261 partition dn string, load the partition from the sam database, identify
1262 the type of the partition (schema, domain, etc) and record the list of
1263 nTDSDSAs that appear in the cross reference attributes
1264 msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations.
1266 :param samdb: sam database to load partition from
1268 attrs = [ "nCName",
1269 "Enabled",
1270 "systemFlags",
1271 "msDS-NC-Replica-Locations",
1272 "msDS-NC-RO-Replica-Locations" ]
1273 try:
1274 res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE,
1275 attrs=attrs)
1277 except ldb.LdbError, (enum, estr):
1278 raise Exception("Unable to find partition for (%s) - (%s)" % (
1279 self.partstr, estr))
1281 msg = res[0]
1282 for k in msg.keys():
1283 if k == "dn":
1284 continue
1286 if k == "Enabled":
1287 if msg[k][0].upper().lstrip().rstrip() == "TRUE":
1288 self.enabled = True
1289 else:
1290 self.enabled = False
1291 continue
1293 if k == "systemFlags":
1294 self.system_flags = int(msg[k][0])
1295 continue
1297 for value in msg[k]:
1298 dsdn = dsdb_Dn(samdb, value)
1299 dnstr = str(dsdn.dn)
1301 if k == "nCName":
1302 self.nc_dnstr = dnstr
1303 continue
1305 if k == "msDS-NC-Replica-Locations":
1306 self.rw_location_list.append(dnstr)
1307 continue
1309 if k == "msDS-NC-RO-Replica-Locations":
1310 self.ro_location_list.append(dnstr)
1311 continue
1313 # Now identify what type of NC this partition
1314 # enumerated
1315 self.identify_by_basedn(samdb)
1317 def is_enabled(self):
1318 """Returns True if partition is enabled
1320 return self.is_enabled
1322 def is_foreign(self):
1323 """Returns True if this is not an Active Directory NC in our
1324 forest but is instead something else (e.g. a foreign NC)
1326 if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0:
1327 return True
1328 else:
1329 return False
1331 def should_be_present(self, target_dsa):
1332 """Tests whether this partition should have an NC replica
1333 on the target dsa. This method returns a tuple of
1334 needed=True/False, ro=True/False, partial=True/False
1336 :param target_dsa: should NC be present on target dsa
1338 needed = False
1339 ro = False
1340 partial = False
1342 # If this is the config, schema, or default
1343 # domain NC for the target dsa then it should
1344 # be present
1345 if self.nc_type == NCType.config or \
1346 self.nc_type == NCType.schema or \
1347 (self.nc_type == NCType.domain and
1348 self.nc_dnstr == target_dsa.default_dnstr):
1349 needed = True
1351 # A writable replica of an application NC should be present
1352 # if there a cross reference to the target DSA exists. Depending
1353 # on whether the DSA is ro we examine which type of cross reference
1354 # to look for (msDS-NC-Replica-Locations or
1355 # msDS-NC-RO-Replica-Locations
1356 if self.nc_type == NCType.application:
1357 if target_dsa.is_ro():
1358 if target_dsa.dsa_dnstr in self.ro_location_list:
1359 needed = True
1360 else:
1361 if target_dsa.dsa_dnstr in self.rw_location_list:
1362 needed = True
1364 # If the target dsa is a gc then a partial replica of a
1365 # domain NC (other than the DSAs default domain) should exist
1366 # if there is also a cross reference for the DSA
1367 if target_dsa.is_gc() and \
1368 self.nc_type == NCType.domain and \
1369 self.nc_dnstr != target_dsa.default_dnstr and \
1370 (target_dsa.dsa_dnstr in self.ro_location_list or
1371 target_dsa.dsa_dnstr in self.rw_location_list):
1372 needed = True
1373 partial = True
1375 # partial NCs are always readonly
1376 if needed and (target_dsa.is_ro() or partial):
1377 ro = True
1379 return needed, ro, partial
1381 def __str__(self):
1382 '''Debug dump string output of class'''
1383 text = "%s" % NamingContext.__str__(self)
1384 text = text + "\n\tpartdn=%s" % self.partstr
1385 for k in self.rw_location_list:
1386 text = text + "\n\tmsDS-NC-Replica-Locations=%s" % k
1387 for k in self.ro_location_list:
1388 text = text + "\n\tmsDS-NC-RO-Replica-Locations=%s" % k
1389 return text
1392 class Site(object):
1393 """An individual site object discovered thru the configuration
1394 naming context. Contains all DSAs that exist within the site
1396 def __init__(self, site_dnstr):
1397 self.site_dnstr = site_dnstr
1398 self.site_options = 0
1399 self.site_topo_generator = None
1400 self.site_topo_failover = 0 # appears to be in minutes
1401 self.dsa_table = {}
1403 def load_site(self, samdb):
1404 """Loads the NTDS Site Settions options attribute for the site
1405 as well as querying and loading all DSAs that appear within
1406 the site.
1408 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1409 attrs = ["options",
1410 "interSiteTopologyFailover",
1411 "interSiteTopologyGenerator"]
1412 try:
1413 res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE,
1414 attrs=attrs)
1415 except ldb.LdbError, (enum, estr):
1416 raise Exception("Unable to find site settings for (%s) - (%s)" %
1417 (ssdn, estr))
1419 msg = res[0]
1420 if "options" in msg:
1421 self.site_options = int(msg["options"][0])
1423 if "interSiteTopologyGenerator" in msg:
1424 self.site_topo_generator = str(msg["interSiteTopologyGenerator"][0])
1426 if "interSiteTopologyFailover" in msg:
1427 self.site_topo_failover = int(msg["interSiteTopologyFailover"][0])
1429 self.load_all_dsa(samdb)
1431 def load_all_dsa(self, samdb):
1432 """Discover all nTDSDSA thru the sites entry and
1433 instantiate and load the DSAs. Each dsa is inserted
1434 into the dsa_table by dn string.
1436 try:
1437 res = samdb.search(self.site_dnstr,
1438 scope=ldb.SCOPE_SUBTREE,
1439 expression="(objectClass=nTDSDSA)")
1440 except ldb.LdbError, (enum, estr):
1441 raise Exception("Unable to find nTDSDSAs - (%s)" % estr)
1443 for msg in res:
1444 dnstr = str(msg.dn)
1446 # already loaded
1447 if dnstr in self.dsa_table.keys():
1448 continue
1450 dsa = DirectoryServiceAgent(dnstr)
1452 dsa.load_dsa(samdb)
1454 # Assign this dsa to my dsa table
1455 # and index by dsa dn
1456 self.dsa_table[dnstr] = dsa
1458 def get_dsa_by_guidstr(self, guidstr):
1459 for dsa in self.dsa_table.values():
1460 if str(dsa.dsa_guid) == guidstr:
1461 return dsa
1462 return None
1464 def get_dsa(self, dnstr):
1465 """Return a previously loaded DSA object by consulting
1466 the sites dsa_table for the provided DSA dn string
1468 :return: None if DSA doesn't exist
1470 if dnstr in self.dsa_table.keys():
1471 return self.dsa_table[dnstr]
1472 return None
1474 def select_istg(self, samdb, mydsa, ro):
1475 """Determine if my DC should be an intersite topology
1476 generator. If my DC is the istg and is both a writeable
1477 DC and the database is opened in write mode then we perform
1478 an originating update to set the interSiteTopologyGenerator
1479 attribute in the NTDS Site Settings object. An RODC always
1480 acts as an ISTG for itself.
1482 # The KCC on an RODC always acts as an ISTG for itself
1483 if mydsa.dsa_is_ro:
1484 mydsa.dsa_is_istg = True
1485 return True
1487 # Find configuration NC replica for my DSA
1488 for c_rep in mydsa.current_rep_table.values():
1489 if c_rep.is_config():
1490 break
1492 if c_rep is None:
1493 raise Exception("Unable to find config NC replica for (%s)" %
1494 mydsa.dsa_dnstr)
1496 # Load repsFrom if not already loaded so we can get the current
1497 # state of the config replica and whether we are getting updates
1498 # from the istg
1499 c_rep.load_repsFrom(samdb)
1501 # From MS-Tech ISTG selection:
1502 # First, the KCC on a writable DC determines whether it acts
1503 # as an ISTG for its site
1505 # Let s be the object such that s!lDAPDisplayName = nTDSDSA
1506 # and classSchema in s!objectClass.
1508 # Let D be the sequence of objects o in the site of the local
1509 # DC such that o!objectCategory = s. D is sorted in ascending
1510 # order by objectGUID.
1512 # Which is a fancy way of saying "sort all the nTDSDSA objects
1513 # in the site by guid in ascending order". Place sorted list
1514 # in D_sort[]
1515 D_sort = []
1516 d_dsa = None
1518 unixnow = int(time.time()) # seconds since 1970
1519 ntnow = unix2nttime(unixnow) # double word number of 100 nanosecond
1520 # intervals since 1600s
1522 for dsa in self.dsa_table.values():
1523 D_sort.append(dsa)
1525 D_sort.sort(sort_dsa_by_guid)
1527 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1528 # if o!interSiteTopologyFailover is 0 or has no value.
1530 # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1531 # so it appears we have to turn f into the same interval
1533 # interSiteTopologyFailover (if set) appears to be in minutes
1534 # so we'll need to convert to senconds and then 100 nanosecond
1535 # intervals
1537 # 10,000,000 is number of 100 nanosecond intervals in a second
1538 if self.site_topo_failover == 0:
1539 f = 2 * 60 * 60 * 10000000
1540 else:
1541 f = self.site_topo_failover * 60 * 10000000
1543 # From MS-Tech ISTG selection:
1544 # If o != NULL and o!interSiteTopologyGenerator is not the
1545 # nTDSDSA object for the local DC and
1546 # o!interSiteTopologyGenerator is an element dj of sequence D:
1548 if self.site_topo_generator is not None and \
1549 self.site_topo_generator in self.dsa_table.keys():
1550 d_dsa = self.dsa_table[self.site_topo_generator]
1551 j_idx = D_sort.index(d_dsa)
1553 if d_dsa is not None and d_dsa is not mydsa:
1554 # From MS-Tech ISTG selection:
1555 # Let c be the cursor in the replUpToDateVector variable
1556 # associated with the NC replica of the config NC such
1557 # that c.uuidDsa = dj!invocationId. If no such c exists
1558 # (No evidence of replication from current ITSG):
1559 # Let i = j.
1560 # Let t = 0.
1562 # Else if the current time < c.timeLastSyncSuccess - f
1563 # (Evidence of time sync problem on current ISTG):
1564 # Let i = 0.
1565 # Let t = 0.
1567 # Else (Evidence of replication from current ITSG):
1568 # Let i = j.
1569 # Let t = c.timeLastSyncSuccess.
1571 # last_success appears to be a double word containing
1572 # number of 100 nanosecond intervals since the 1600s
1573 if d_dsa.dsa_ivid != c_rep.source_dsa_invocation_id:
1574 i_idx = j_idx
1575 t_time = 0
1577 elif ntnow < (c_rep.last_success - f):
1578 i_idx = 0
1579 t_time = 0
1580 else:
1581 i_idx = j_idx
1582 t_time = c_rep.last_success
1584 # Otherwise (Nominate local DC as ISTG):
1585 # Let i be the integer such that di is the nTDSDSA
1586 # object for the local DC.
1587 # Let t = the current time.
1588 else:
1589 i_idx = D_sort.index(mydsa)
1590 t_time = ntnow
1592 # Compute a function that maintains the current ISTG if
1593 # it is alive, cycles through other candidates if not.
1595 # Let k be the integer (i + ((current time - t) /
1596 # o!interSiteTopologyFailover)) MOD |D|.
1598 # Note: We don't want to divide by zero here so they must
1599 # have meant "f" instead of "o!interSiteTopologyFailover"
1600 k_idx = (i_idx + ((ntnow - t_time) / f)) % len(D_sort)
1602 # The local writable DC acts as an ISTG for its site if and
1603 # only if dk is the nTDSDSA object for the local DC. If the
1604 # local DC does not act as an ISTG, the KCC skips the
1605 # remainder of this task.
1606 d_dsa = D_sort[k_idx]
1607 d_dsa.dsa_is_istg = True
1609 # Update if we are the ISTG, otherwise return
1610 if d_dsa is not mydsa:
1611 return False
1613 # Nothing to do
1614 if self.site_topo_generator == mydsa.dsa_dnstr:
1615 return True
1617 self.site_topo_generator = mydsa.dsa_dnstr
1619 # If readonly database then do not perform a
1620 # persistent update
1621 if ro:
1622 return True
1624 # Perform update to the samdb
1625 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1627 m = ldb.Message()
1628 m.dn = ldb.Dn(samdb, ssdn)
1630 m["interSiteTopologyGenerator"] = \
1631 ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
1632 "interSiteTopologyGenerator")
1633 try:
1634 samdb.modify(m)
1636 except ldb.LdbError, estr:
1637 raise Exception(
1638 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1639 (ssdn, estr))
1640 return True
1642 def is_intrasite_topology_disabled(self):
1643 '''Returns True if intra-site topology is disabled for site'''
1644 if (self.site_options &
1645 dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0:
1646 return True
1647 return False
1649 def is_intersite_topology_disabled(self):
1650 '''Returns True if inter-site topology is disabled for site'''
1651 if (self.site_options &
1652 dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED) != 0:
1653 return True
1654 return False
1656 def is_random_bridgehead_disabled(self):
1657 '''Returns True if selection of random bridgehead is disabled'''
1658 if (self.site_options &
1659 dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0:
1660 return True
1661 return False
1663 def is_detect_stale_disabled(self):
1664 '''Returns True if detect stale is disabled for site'''
1665 if (self.site_options &
1666 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0:
1667 return True
1668 return False
1670 def is_cleanup_ntdsconn_disabled(self):
1671 '''Returns True if NTDS Connection cleanup is disabled for site'''
1672 if (self.site_options &
1673 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0:
1674 return True
1675 return False
1677 def same_site(self, dsa):
1678 '''Return True if dsa is in this site'''
1679 if self.get_dsa(dsa.dsa_dnstr):
1680 return True
1681 return False
1683 def __str__(self):
1684 '''Debug dump string output of class'''
1685 text = "%s:" % self.__class__.__name__
1686 text = text + "\n\tdn=%s" % self.site_dnstr
1687 text = text + "\n\toptions=0x%X" % self.site_options
1688 text = text + "\n\ttopo_generator=%s" % self.site_topo_generator
1689 text = text + "\n\ttopo_failover=%d" % self.site_topo_failover
1690 for key, dsa in self.dsa_table.items():
1691 text = text + "\n%s" % dsa
1692 return text
1695 class GraphNode(object):
1696 """A graph node describing a set of edges that should be directed to it.
1698 Each edge is a connection for a particular naming context replica directed
1699 from another node in the forest to this node.
1702 def __init__(self, dsa_dnstr, max_node_edges):
1703 """Instantiate the graph node according to a DSA dn string
1705 :param max_node_edges: maximum number of edges that should ever
1706 be directed to the node
1708 self.max_edges = max_node_edges
1709 self.dsa_dnstr = dsa_dnstr
1710 self.edge_from = []
1712 def __str__(self):
1713 text = "%s:" % self.__class__.__name__
1714 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
1715 text = text + "\n\tmax_edges=%d" % self.max_edges
1717 for i, edge in enumerate(self.edge_from):
1718 text = text + "\n\tedge_from[%d]=%s" % (i, edge)
1719 return text
1721 def add_edge_from(self, from_dsa_dnstr):
1722 """Add an edge from the dsa to our graph nodes edge from list
1724 :param from_dsa_dnstr: the dsa that the edge emanates from
1726 assert from_dsa_dnstr is not None
1728 # No edges from myself to myself
1729 if from_dsa_dnstr == self.dsa_dnstr:
1730 return False
1731 # Only one edge from a particular node
1732 if from_dsa_dnstr in self.edge_from:
1733 return False
1734 # Not too many edges
1735 if len(self.edge_from) >= self.max_edges:
1736 return False
1737 self.edge_from.append(from_dsa_dnstr)
1738 return True
1740 def add_edges_from_connections(self, dsa):
1741 """For each nTDSConnection object associated with a particular
1742 DSA, we test if it implies an edge to this graph node (i.e.
1743 the "fromServer" attribute). If it does then we add an
1744 edge from the server unless we are over the max edges for this
1745 graph node
1747 :param dsa: dsa with a dnstr equivalent to his graph node
1749 for dnstr, connect in dsa.connect_table.items():
1750 self.add_edge_from(connect.from_dnstr)
1752 def add_connections_from_edges(self, dsa):
1753 """For each edge directed to this graph node, ensure there
1754 is a corresponding nTDSConnection object in the dsa.
1756 for edge_dnstr in self.edge_from:
1757 connect = dsa.get_connection_by_from_dnstr(edge_dnstr)
1759 # For each edge directed to the NC replica that
1760 # "should be present" on the local DC, the KCC determines
1761 # whether an object c exists such that:
1763 # c is a child of the DC's nTDSDSA object.
1764 # c.objectCategory = nTDSConnection
1766 # Given the NC replica ri from which the edge is directed,
1767 # c.fromServer is the dsname of the nTDSDSA object of
1768 # the DC on which ri "is present".
1770 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1771 if connect and not connect.is_rodc_topology():
1772 exists = True
1773 else:
1774 exists = False
1776 # if no such object exists then the KCC adds an object
1777 # c with the following attributes
1778 if exists:
1779 return
1781 # Generate a new dnstr for this nTDSConnection
1782 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1783 flags = dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME + \
1784 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE
1786 dsa.create_connection(opt, flags, None, edge_dnstr, None)
1788 def has_sufficient_edges(self):
1789 '''Return True if we have met the maximum "from edges" criteria'''
1790 if len(self.edge_from) >= self.max_edges:
1791 return True
1792 return False
1795 class Transport(object):
1796 """Class defines a Inter-site transport found under Sites
1799 def __init__(self, dnstr):
1800 self.dnstr = dnstr
1801 self.options = 0
1802 self.guid = None
1803 self.name = None
1804 self.address_attr = None
1805 self.bridgehead_list = []
1807 def __str__(self):
1808 '''Debug dump string output of Transport object'''
1810 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
1811 text = text + "\n\tguid=%s" % str(self.guid)
1812 text = text + "\n\toptions=%d" % self.options
1813 text = text + "\n\taddress_attr=%s" % self.address_attr
1814 text = text + "\n\tname=%s" % self.name
1815 for dnstr in self.bridgehead_list:
1816 text = text + "\n\tbridgehead_list=%s" % dnstr
1818 return text
1820 def load_transport(self, samdb):
1821 """Given a Transport object with an prior initialization
1822 for the object's DN, search for the DN and load attributes
1823 from the samdb.
1825 attrs = [ "objectGUID",
1826 "options",
1827 "name",
1828 "bridgeheadServerListBL",
1829 "transportAddressAttribute" ]
1830 try:
1831 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
1832 attrs=attrs)
1834 except ldb.LdbError, (enum, estr):
1835 raise Exception("Unable to find Transport for (%s) - (%s)" %
1836 (self.dnstr, estr))
1838 msg = res[0]
1839 self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
1840 msg["objectGUID"][0]))
1842 if "options" in msg:
1843 self.options = int(msg["options"][0])
1845 if "transportAddressAttribute" in msg:
1846 self.address_attr = str(msg["transportAddressAttribute"][0])
1848 if "name" in msg:
1849 self.name = str(msg["name"][0])
1851 if "bridgeheadServerListBL" in msg:
1852 for value in msg["bridgeheadServerListBL"]:
1853 dsdn = dsdb_Dn(samdb, value)
1854 dnstr = str(dsdn.dn)
1855 if dnstr not in self.bridgehead_list:
1856 self.bridgehead_list.append(dnstr)
1859 class RepsFromTo(object):
1860 """Class encapsulation of the NDR repsFromToBlob.
1862 Removes the necessity of external code having to
1863 understand about other_info or manipulation of
1864 update flags.
1866 def __init__(self, nc_dnstr=None, ndr_blob=None):
1868 self.__dict__['to_be_deleted'] = False
1869 self.__dict__['nc_dnstr'] = nc_dnstr
1870 self.__dict__['update_flags'] = 0x0
1872 # WARNING:
1874 # There is a very subtle bug here with python
1875 # and our NDR code. If you assign directly to
1876 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1877 # then a proper python GC reference count is not
1878 # maintained.
1880 # To work around this we maintain an internal
1881 # reference to "dns_name(x)" and "other_info" elements
1882 # of repsFromToBlob. This internal reference
1883 # is hidden within this class but it is why you
1884 # see statements like this below:
1886 # self.__dict__['ndr_blob'].ctr.other_info = \
1887 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1889 # That would appear to be a redundant assignment but
1890 # it is necessary to hold a proper python GC reference
1891 # count.
1892 if ndr_blob is None:
1893 self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
1894 self.__dict__['ndr_blob'].version = 0x1
1895 self.__dict__['dns_name1'] = None
1896 self.__dict__['dns_name2'] = None
1898 self.__dict__['ndr_blob'].ctr.other_info = \
1899 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1901 else:
1902 self.__dict__['ndr_blob'] = ndr_blob
1903 self.__dict__['other_info'] = ndr_blob.ctr.other_info
1905 if ndr_blob.version == 0x1:
1906 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
1907 self.__dict__['dns_name2'] = None
1908 else:
1909 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
1910 self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
1912 def __str__(self):
1913 '''Debug dump string output of class'''
1915 text = "%s:" % self.__class__.__name__
1916 text = text + "\n\tdnstr=%s" % self.nc_dnstr
1917 text = text + "\n\tupdate_flags=0x%X" % self.update_flags
1919 text = text + "\n\tversion=%d" % self.version
1920 text = text + "\n\tsource_dsa_obj_guid=%s" % \
1921 str(self.source_dsa_obj_guid)
1922 text = text + "\n\tsource_dsa_invocation_id=%s" % \
1923 str(self.source_dsa_invocation_id)
1924 text = text + "\n\ttransport_guid=%s" % \
1925 str(self.transport_guid)
1926 text = text + "\n\treplica_flags=0x%X" % \
1927 self.replica_flags
1928 text = text + "\n\tconsecutive_sync_failures=%d" % \
1929 self.consecutive_sync_failures
1930 text = text + "\n\tlast_success=%s" % \
1931 self.last_success
1932 text = text + "\n\tlast_attempt=%s" % \
1933 self.last_attempt
1934 text = text + "\n\tdns_name1=%s" % \
1935 str(self.dns_name1)
1936 text = text + "\n\tdns_name2=%s" % \
1937 str(self.dns_name2)
1938 text = text + "\n\tschedule[ "
1939 for slot in self.schedule:
1940 text = text + "0x%X " % slot
1941 text = text + "]"
1943 return text
1945 def __setattr__(self, item, value):
1947 if item in [ 'schedule', 'replica_flags', 'transport_guid',
1948 'source_dsa_obj_guid', 'source_dsa_invocation_id',
1949 'consecutive_sync_failures', 'last_success',
1950 'last_attempt' ]:
1952 if item in ['replica_flags']:
1953 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
1954 elif item in ['schedule']:
1955 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
1957 setattr(self.__dict__['ndr_blob'].ctr, item, value)
1959 elif item in ['dns_name1']:
1960 self.__dict__['dns_name1'] = value
1962 if self.__dict__['ndr_blob'].version == 0x1:
1963 self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
1964 self.__dict__['dns_name1']
1965 else:
1966 self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
1967 self.__dict__['dns_name1']
1969 elif item in ['dns_name2']:
1970 self.__dict__['dns_name2'] = value
1972 if self.__dict__['ndr_blob'].version == 0x1:
1973 raise AttributeError(item)
1974 else:
1975 self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
1976 self.__dict__['dns_name2']
1978 elif item in ['nc_dnstr']:
1979 self.__dict__['nc_dnstr'] = value
1981 elif item in ['to_be_deleted']:
1982 self.__dict__['to_be_deleted'] = value
1984 elif item in ['version']:
1985 raise AttributeError("Attempt to set readonly attribute %s" % item)
1986 else:
1987 raise AttributeError("Unknown attribute %s" % item)
1989 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
1991 def __getattr__(self, item):
1992 """Overload of RepsFromTo attribute retrieval.
1994 Allows external code to ignore substructures within the blob
1996 if item in [ 'schedule', 'replica_flags', 'transport_guid',
1997 'source_dsa_obj_guid', 'source_dsa_invocation_id',
1998 'consecutive_sync_failures', 'last_success',
1999 'last_attempt' ]:
2000 return getattr(self.__dict__['ndr_blob'].ctr, item)
2002 elif item in ['version']:
2003 return self.__dict__['ndr_blob'].version
2005 elif item in ['dns_name1']:
2006 if self.__dict__['ndr_blob'].version == 0x1:
2007 return self.__dict__['ndr_blob'].ctr.other_info.dns_name
2008 else:
2009 return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
2011 elif item in ['dns_name2']:
2012 if self.__dict__['ndr_blob'].version == 0x1:
2013 raise AttributeError(item)
2014 else:
2015 return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
2017 elif item in ['to_be_deleted']:
2018 return self.__dict__['to_be_deleted']
2020 elif item in ['nc_dnstr']:
2021 return self.__dict__['nc_dnstr']
2023 elif item in ['update_flags']:
2024 return self.__dict__['update_flags']
2026 raise AttributeError("Unknwown attribute %s" % item)
2028 def is_modified(self):
2029 return (self.update_flags != 0x0)
2031 def set_unmodified(self):
2032 self.__dict__['update_flags'] = 0x0
2035 class SiteLink(object):
2036 """Class defines a site link found under sites
2039 def __init__(self, dnstr):
2040 self.dnstr = dnstr
2041 self.options = 0
2042 self.system_flags = 0
2043 self.cost = 0
2044 self.schedule = None
2045 self.interval = None
2046 self.site_list = []
2048 def __str__(self):
2049 '''Debug dump string output of Transport object'''
2051 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
2052 text = text + "\n\toptions=%d" % self.options
2053 text = text + "\n\tsystem_flags=%d" % self.system_flags
2054 text = text + "\n\tcost=%d" % self.cost
2055 text = text + "\n\tinterval=%s" % self.interval
2057 if self.schedule is not None:
2058 text = text + "\n\tschedule.size=%s" % self.schedule.size
2059 text = text + "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
2060 text = text + "\n\tschedule.numberOfSchedules=%s" % \
2061 self.schedule.numberOfSchedules
2063 for i, header in enumerate(self.schedule.headerArray):
2064 text = text + "\n\tschedule.headerArray[%d].type=%d" % \
2065 (i, header.type)
2066 text = text + "\n\tschedule.headerArray[%d].offset=%d" % \
2067 (i, header.offset)
2068 text = text + "\n\tschedule.dataArray[%d].slots[ " % i
2069 for slot in self.schedule.dataArray[i].slots:
2070 text = text + "0x%X " % slot
2071 text = text + "]"
2073 for dnstr in self.site_list:
2074 text = text + "\n\tsite_list=%s" % dnstr
2075 return text
2077 def load_sitelink(self, samdb):
2078 """Given a siteLink object with an prior initialization
2079 for the object's DN, search for the DN and load attributes
2080 from the samdb.
2082 attrs = [ "options",
2083 "systemFlags",
2084 "cost",
2085 "schedule",
2086 "replInterval",
2087 "siteList" ]
2088 try:
2089 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
2090 attrs=attrs)
2092 except ldb.LdbError, (enum, estr):
2093 raise Exception("Unable to find SiteLink for (%s) - (%s)" %
2094 (self.dnstr, estr))
2096 msg = res[0]
2098 if "options" in msg:
2099 self.options = int(msg["options"][0])
2101 if "systemFlags" in msg:
2102 self.system_flags = int(msg["systemFlags"][0])
2104 if "cost" in msg:
2105 self.cost = int(msg["cost"][0])
2107 if "replInterval" in msg:
2108 self.interval = int(msg["replInterval"][0])
2110 if "siteList" in msg:
2111 for value in msg["siteList"]:
2112 dsdn = dsdb_Dn(samdb, value)
2113 dnstr = str(dsdn.dn)
2114 if dnstr not in self.site_list:
2115 self.site_list.append(dnstr)
2117 def is_sitelink(self, site1_dnstr, site2_dnstr):
2118 """Given a siteLink object, determine if it is a link
2119 between the two input site DNs
2121 if site1_dnstr in self.site_list and site2_dnstr in self.site_list:
2122 return True
2123 return False
2126 class VertexColor(object):
2127 (unknown, white, black, red) = range(0, 4)
2130 class Vertex(object):
2131 """Class encapsulation of a Site Vertex in the
2132 intersite topology replication algorithm
2134 def __init__(self, site, part):
2135 self.site = site
2136 self.part = part
2137 self.color = VertexColor.unknown
2139 def color_vertex(self):
2140 """Color each vertex to indicate which kind of NC
2141 replica it contains
2143 # IF s contains one or more DCs with full replicas of the
2144 # NC cr!nCName
2145 # SET v.Color to COLOR.RED
2146 # ELSEIF s contains one or more partial replicas of the NC
2147 # SET v.Color to COLOR.BLACK
2148 #ELSE
2149 # SET v.Color to COLOR.WHITE
2151 # set to minimum (no replica)
2152 self.color = VertexColor.white
2154 for dnstr, dsa in self.site.dsa_table.items():
2155 rep = dsa.get_current_replica(self.part.nc_dnstr)
2156 if rep is None:
2157 continue
2159 # We have a full replica which is the largest
2160 # value so exit
2161 if not rep.is_partial():
2162 self.color = VertexColor.red
2163 break
2164 else:
2165 self.color = VertexColor.black
2167 def is_red(self):
2168 assert(self.color != VertexColor.unknown)
2169 return (self.color == VertexColor.red)
2171 def is_black(self):
2172 assert(self.color != VertexColor.unknown)
2173 return (self.color == VertexColor.black)
2175 def is_white(self):
2176 assert(self.color != VertexColor.unknown)
2177 return (self.color == VertexColor.white)
2179 ##################################################
2180 # Global Functions
2181 ##################################################
2182 def sort_dsa_by_guid(dsa1, dsa2):
2183 return cmp(dsa1.dsa_guid, dsa2.dsa_guid)