ad0f4e547008166f3d1ea73aa7d249789c970363
[obnox/samba/samba-obnox.git] / python / samba / kcc_utils.py
1 # KCC topology utilities
2 #
3 # Copyright (C) Dave Craft 2011
4 # Copyright (C) Jelmer Vernooij 2011
5 #
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.
10 #
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.
15 #
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/>.
18
19 import ldb
20 import uuid
21 import time
22
23 from samba import dsdb, unix2nttime
24 from samba.dcerpc import (
25     drsblobs,
26     drsuapi,
27     misc,
28     )
29 from samba.common import dsdb_Dn
30 from samba.ndr import (ndr_unpack, ndr_pack)
31
32
33 class NCType(object):
34     (unknown, schema, domain, config, application) = range(0, 5)
35
36
37 class NamingContext(object):
38     """Base class for a naming context.
39
40     Holds the DN, GUID, SID (if available) and type of the DN.
41     Subclasses may inherit from this and specialize
42     """
43
44     def __init__(self, nc_dnstr):
45         """Instantiate a NamingContext
46
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
53
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)
59
60         if self.nc_sid is None:
61             text = text + "\n\tnc_sid=<absent>"
62         else:
63             text = text + "\n\tnc_sid=<present>"
64
65         text = text + "\n\tnc_type=%s"  % self.nc_type
66         return text
67
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)
74
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]
84
85         assert self.nc_guid is not None
86
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
91
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
96
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
101
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
106
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
110         """
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)
116
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
129
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.
136
137         :param attr: attr of nTDSDSA object where NC DN appears
138         """
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
144
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
149
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)
156
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)
161
162
163 class NCReplica(NamingContext):
164     """Naming context replica that is relative to a specific DSA.
165
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.
168     """
169
170     def __init__(self, dsa_dnstr, dsa_guid, nc_dnstr):
171         """Instantiate a Naming Context Replica
172
173         :param dsa_guid: GUID of DSA where replica appears
174         :param nc_dnstr: NC dn string
175         """
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
182
183         self.rep_fsmo_role_owner = None
184
185         # RepsFromTo tuples
186         self.rep_repsFrom = []
187
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
194
195         # Call my super class we inherited from
196         NamingContext.__init__(self, nc_dnstr)
197
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
208
209         for rep in self.rep_repsFrom:
210             text = text + "\n%s" % rep
211
212         return "%s\n%s" % (NamingContext.__str__(self), text)
213
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
220
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)
225
226         :param attr: attr of nTDSDSA object where NC DN appears
227         """
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
233
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
239
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
250
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
254
255         # RODC only
256         elif attr == "msDS-hasFullReplicaNCs":
257             self.rep_present_criteria_one = True
258             self.rep_ro = True
259
260         # Not RODC
261         elif attr == "msDS-hasMasterNCs":
262             self.rep_ro = False
263
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)
268
269     def is_default(self):
270         """Whether this is a default domain for the dsa that this NC appears on
271         """
272         return self.rep_default
273
274     def is_ro(self):
275         '''Return True if NC replica is read only'''
276         return self.rep_ro
277
278     def is_partial(self):
279         '''Return True if NC replica is partial'''
280         return self.rep_partial
281
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)
288         """
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
293
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
299         """
300         try:
301             res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
302                                attrs=[ "repsFrom" ])
303
304         except ldb.LdbError, (enum, estr):
305             raise Exception("Unable to find NC for (%s) - (%s)" %
306                             (self.nc_dnstr, estr))
307
308         msg = res[0]
309
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)
316
317     def commit_repsFrom(self, samdb, ro=False):
318         """Commit repsFrom to the database"""
319
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.
324         #
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 = []
333
334         for repsFrom in self.rep_repsFrom:
335
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
344
345             if repsFrom.is_modified():
346                 repsFrom.set_unmodified()
347                 modify = True
348
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))
354
355         # Now delete these from our list of rep_repsFrom
356         for repsFrom in delreps:
357             self.rep_repsFrom.remove(repsFrom)
358         delreps = []
359
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
366
367         m = ldb.Message()
368         m.dn = ldb.Dn(samdb, self.nc_dnstr)
369
370         m["repsFrom"] = \
371             ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsFrom")
372
373         try:
374             samdb.modify(m)
375
376         except ldb.LdbError, estr:
377             raise Exception("Could not set repsFrom for (%s) - (%s)" %
378                             (self.dsa_dnstr, estr))
379
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
389
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
399
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.
403         """
404         try:
405             res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
406                                attrs=[ "fSMORoleOwner" ])
407
408         except ldb.LdbError, (enum, estr):
409             raise Exception("Unable to find NC for (%s) - (%s)" %
410                             (self.nc_dnstr, estr))
411
412         msg = res[0]
413
414         # Possibly no fSMORoleOwner
415         if "fSMORoleOwner" in msg:
416             self.rep_fsmo_role_owner = msg["fSMORoleOwner"]
417
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
423
424
425 class DirectoryServiceAgent(object):
426
427     def __init__(self, dsa_dnstr):
428         """Initialize DSA class.
429
430         Class is subsequently fully populated by calling the load_dsa() method
431
432         :param dsa_dnstr:  DN of the nTDSDSA
433         """
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
442
443         # NCReplicas for this dsa that are "present"
444         # Indexed by DN string of naming context
445         self.current_rep_table = {}
446
447         # NCReplicas for this dsa that "should be present"
448         # Indexed by DN string of naming context
449         self.needed_rep_table = {}
450
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 = {}
455
456     def __str__(self):
457         '''Debug dump string output of class'''
458
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)
466
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()
470
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()
477
478         return text
479
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
485
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
490
491     def is_ro(self):
492         '''Returns True if dsa a read only domain controller'''
493         return self.dsa_is_ro
494
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
500
501     def is_minimum_behavior(self, version):
502         """Is dsa at minimum windows level greater than or equal to (version)
503
504         :param version: Windows version to test against
505             (e.g. DS_DOMAIN_FUNCTION_2008)
506         """
507         if self.dsa_behavior >= version:
508             return True
509         return False
510
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
516
517     def get_rep_tables(self):
518         """Return DSA current and needed replica tables
519         """
520         return self.current_rep_table, self.needed_rep_table
521
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
526
527     def load_dsa(self, samdb):
528         """Load a DSA from the samdb.
529
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.
533         """
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)
542
543         except ldb.LdbError, (enum, estr):
544             raise Exception("Unable to find nTDSDSA for (%s) - (%s)" %
545                             (self.dsa_dnstr, estr))
546
547         msg = res[0]
548         self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID",
549                                   msg["objectGUID"][0]))
550
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]))
556
557         if "options" in msg:
558             self.options = int(msg["options"][0])
559
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
564
565         if "msDS-Behavior-Version" in msg:
566             self.dsa_behavior = int(msg['msDS-Behavior-Version'][0])
567
568         # Load the NC replicas that are enumerated on this dsa
569         self.load_current_replica_table(samdb)
570
571         # Load the nTDSConnection that are enumerated on this dsa
572         self.load_connection_table(samdb)
573
574     def load_current_replica_table(self, samdb):
575         """Method to load the NC replica's listed for DSA object.
576
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.
583
584         :param samdb: database to query for DSA replica list
585         """
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)
601
602         except ldb.LdbError, (enum, estr):
603             raise Exception("Unable to find nTDSDSA NCs for (%s) - (%s)" %
604                             (self.dsa_dnstr, estr))
605
606         # The table of NCs for the dsa we are searching
607         tmp_table = {}
608
609         # We should get one response to our query here for
610         # the ntds that we requested
611         if len(res[0]) > 0:
612
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
621
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)
631
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]
637
638                     if k == "msDS-HasInstantiatedNCs":
639                         rep.set_instantiated_flags(flags)
640                         continue
641
642                     rep.identify_by_dsa_attr(samdb, k)
643
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)
650
651         # Assign our newly built NC replica table to this dsa
652         self.current_rep_table = tmp_table
653
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
657         """
658         if not rep.nc_dnstr in self.needed_rep_table.keys():
659             self.needed_rep_table[rep.nc_dnstr] = rep
660
661     def load_connection_table(self, samdb):
662         """Method to load the nTDSConnections listed for DSA object.
663
664         :param samdb: database to query for DSA connection list
665         """
666         try:
667             res = samdb.search(base=self.dsa_dnstr,
668                                scope=ldb.SCOPE_SUBTREE,
669                                expression="(objectClass=nTDSConnection)")
670
671         except ldb.LdbError, (enum, estr):
672             raise Exception("Unable to find nTDSConnection for (%s) - (%s)" %
673                             (self.dsa_dnstr, estr))
674
675         for msg in res:
676             dnstr = str(msg.dn)
677
678             # already loaded
679             if dnstr in self.connect_table.keys():
680                 continue
681
682             connect = NTDSConnection(dnstr)
683
684             connect.load_connection(samdb)
685             self.connect_table[dnstr] = connect
686
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
692
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)
696         """
697         delconn = []
698
699         for dnstr, connect in self.connect_table.items():
700             if connect.to_be_added:
701                 connect.commit_added(samdb, ro)
702
703             if connect.to_be_modified:
704                 connect.commit_modified(samdb, ro)
705
706             if connect.to_be_deleted:
707                 connect.commit_deleted(samdb, ro)
708                 delconn.append(dnstr)
709
710         # Now delete the connection from the table
711         for dnstr in delconn:
712             del self.connect_table[dnstr]
713
714     def add_connection(self, dnstr, connect):
715         assert dnstr not in self.connect_table.keys()
716         self.connect_table[dnstr] = connect
717
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.
722
723         :param from_dnstr: search for this from server entry
724         """
725         for dnstr, connect in self.connect_table.items():
726             if connect.get_from_dnstr() == from_dnstr:
727                 return connect
728         return None
729
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
739
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
749
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
759
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()
765         """
766         dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
767
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
774
775         if transport is not None:
776             connect.transport_dnstr = transport.dnstr
777
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()
784
785             connect.schedule.size = 188
786             connect.schedule.bandwidth = 0
787             connect.schedule.numberOfSchedules = 1
788
789             header = drsblobs.scheduleHeader()
790             header.type = 0
791             header.offset = 20
792
793             connect.schedule.headerArray = [ header ]
794
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
802
803             connect.schedule.dataArray = [ data ]
804
805         self.add_connection(dnstr, connect);
806         return connect
807
808
809 class NTDSConnection(object):
810     """Class defines a nTDSConnection found under a DSA
811     """
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
826
827     def __str__(self):
828         '''Debug dump string output of NTDSConnection object'''
829
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
839
840         if self.guid is not None:
841             text = text + "\n\tguid=%s" % str(self.guid)
842
843         if self.transport_guid is not None:
844             text = text + "\n\ttransport_guid=%s" % str(self.transport_guid)
845
846         text = text + "\n\tfrom_dn=%s" % self.from_dnstr
847
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
853
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 + "]"
863
864         return text
865
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.
870         """
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)
882
883         except ldb.LdbError, (enum, estr):
884             raise Exception("Unable to find nTDSConnection for (%s) - (%s)" %
885                             (self.dnstr, estr))
886
887         msg = res[0]
888
889         if "options" in msg:
890             self.options = int(msg["options"][0])
891
892         if "enabledConnection" in msg:
893             if msg["enabledConnection"][0].upper().lstrip().rstrip() == "TRUE":
894                 self.enabled = True
895
896         if "systemFlags" in msg:
897             self.system_flags = int(msg["systemFlags"][0])
898
899         if "objectGUID" in msg:
900             self.guid = \
901                 misc.GUID(samdb.schema_format_value("objectGUID",
902                                                     msg["objectGUID"][0]))
903
904         if "transportType" in msg:
905             dsdn = dsdb_Dn(samdb, msg["tranportType"][0])
906             self.load_connection_transport(samdb, str(dsdn.dn))
907
908         if "schedule" in msg:
909             self.schedule = ndr_unpack(drsblobs.replSchedule, msg["schedule"][0])
910
911         if "whenCreated" in msg:
912             self.whenCreated = ldb.string_to_time(msg["whenCreated"][0])
913
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
918
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
922
923         :param tdnstr: transport DN to load
924         """
925         attrs = [ "objectGUID" ]
926         try:
927             res = samdb.search(base=tdnstr,
928                                scope=ldb.SCOPE_BASE, attrs=attrs)
929
930         except ldb.LdbError, (enum, estr):
931             raise Exception("Unable to find transport (%s)" %
932                             (tdnstr, estr))
933
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
942
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
947         """
948         assert self.to_be_deleted
949         self.to_be_deleted = False
950
951         # No database modification requested
952         if ro:
953             return
954
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))
960
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
965         """
966         assert self.to_be_added
967         self.to_be_added = False
968
969         # No database modification requested
970         if ro:
971             return
972
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
980
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)
988
989         if self.enabled:
990             enablestr = "TRUE"
991         else:
992             enablestr = "FALSE"
993
994         # Prepare a message for adding to the samdb
995         m = ldb.Message()
996         m.dn = ldb.Dn(samdb, self.dnstr)
997
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")
1013
1014         if self.transport_dnstr is not None:
1015             m["transportType"] = \
1016                 ldb.MessageElement(str(self.transport_dnstr), ldb.FLAG_MOD_ADD,
1017                                    "transportType")
1018
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))
1028
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
1033         """
1034         assert self.to_be_modified
1035         self.to_be_modified = False
1036
1037         # No database modification requested
1038         if ro:
1039             return
1040
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
1046
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)
1056
1057         if self.enabled:
1058             enablestr = "TRUE"
1059         else:
1060             enablestr = "FALSE"
1061
1062         # Prepare a message for modifying the samdb
1063         m = ldb.Message()
1064         m.dn = ldb.Dn(samdb, self.dnstr)
1065
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")
1078
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")
1086
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))
1099
1100     def set_modified(self, truefalse):
1101         self.to_be_modified = truefalse
1102
1103     def set_added(self, truefalse):
1104         self.to_be_added = truefalse
1105
1106     def set_deleted(self, truefalse):
1107         self.to_be_deleted = truefalse
1108
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
1112         """
1113         if self.schedule is None or self.schedule.dataArray[0] is None:
1114             return False
1115
1116         for slot in self.schedule.dataArray[0].slots:
1117            if (slot & 0x0F) != 0x0:
1118                return True
1119         return False
1120
1121     def is_equivalent_schedule(self, sched):
1122         """Returns True if our schedule is equivalent to the input
1123         comparison schedule.
1124
1125         :param shed: schedule to compare to
1126         """
1127         if self.schedule is not None:
1128             if sched is None:
1129                return False
1130         elif sched is None:
1131             return True
1132
1133         if (self.schedule.size != sched.size or
1134             self.schedule.bandwidth != sched.bandwidth or
1135             self.schedule.numberOfSchedules != sched.numberOfSchedules):
1136             return False
1137
1138         for i, header in enumerate(self.schedule.headerArray):
1139
1140             if self.schedule.headerArray[i].type != sched.headerArray[i].type:
1141                 return False
1142
1143             if self.schedule.headerArray[i].offset != \
1144                sched.headerArray[i].offset:
1145                 return False
1146
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
1152
1153     def convert_schedule_to_repltimes(self):
1154         """Convert NTDS Connection schedule to replTime schedule.
1155
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.
1167         """
1168         times = [0x11] * 84
1169
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
1176
1177     def is_rodc_topology(self):
1178         """Returns True if NTDS Connection specifies RODC
1179         topology only
1180         """
1181         if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
1182             return False
1183         return True
1184
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
1188         """
1189         if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0:
1190             return False
1191         return True
1192
1193     def is_override_notify_default(self):
1194         """Returns True if NTDS Connection should override notify default
1195         """
1196         if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0:
1197             return False
1198         return True
1199
1200     def is_use_notify(self):
1201         """Returns True if NTDS Connection should use notify
1202         """
1203         if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0:
1204             return False
1205         return True
1206
1207     def is_twoway_sync(self):
1208         """Returns True if NTDS Connection should use twoway sync
1209         """
1210         if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0:
1211             return False
1212         return True
1213
1214     def is_intersite_compression_disabled(self):
1215         """Returns True if NTDS Connection intersite compression
1216         is disabled
1217         """
1218         if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0:
1219             return False
1220         return True
1221
1222     def is_user_owned_schedule(self):
1223         """Returns True if NTDS Connection has a user owned schedule
1224         """
1225         if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0:
1226             return False
1227         return True
1228
1229     def is_enabled(self):
1230         """Returns True if NTDS Connection is enabled
1231         """
1232         return self.enabled
1233
1234     def get_from_dnstr(self):
1235         '''Return fromServer dn string attribute'''
1236         return self.from_dnstr
1237
1238
1239 class Partition(NamingContext):
1240     """A naming context discovered thru Partitions DN of the config schema.
1241
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
1245     """
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 = []
1252
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)
1257
1258
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.
1265
1266         :param samdb: sam database to load partition from
1267         """
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)
1276
1277         except ldb.LdbError, (enum, estr):
1278             raise Exception("Unable to find partition for (%s) - (%s)" % (
1279                             self.partstr, estr))
1280
1281         msg = res[0]
1282         for k in msg.keys():
1283             if k == "dn":
1284                 continue
1285
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
1292
1293             if k == "systemFlags":
1294                 self.system_flags = int(msg[k][0])
1295                 continue
1296
1297             for value in msg[k]:
1298                 dsdn = dsdb_Dn(samdb, value)
1299                 dnstr = str(dsdn.dn)
1300
1301                 if k == "nCName":
1302                     self.nc_dnstr = dnstr
1303                     continue
1304
1305                 if k == "msDS-NC-Replica-Locations":
1306                     self.rw_location_list.append(dnstr)
1307                     continue
1308
1309                 if k == "msDS-NC-RO-Replica-Locations":
1310                     self.ro_location_list.append(dnstr)
1311                     continue
1312
1313         # Now identify what type of NC this partition
1314         # enumerated
1315         self.identify_by_basedn(samdb)
1316
1317     def is_enabled(self):
1318         """Returns True if partition is enabled
1319         """
1320         return self.is_enabled
1321
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)
1325         """
1326         if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0:
1327             return True
1328         else:
1329             return False
1330
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
1335
1336         :param target_dsa: should NC be present on target dsa
1337         """
1338         needed = False
1339         ro = False
1340         partial = False
1341
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
1350
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
1363
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
1374
1375         # partial NCs are always readonly
1376         if needed and (target_dsa.is_ro() or partial):
1377             ro = True
1378
1379         return needed, ro, partial
1380
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
1390
1391
1392 class Site(object):
1393     """An individual site object discovered thru the configuration
1394     naming context.  Contains all DSAs that exist within the site
1395     """
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 = {}
1402
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.
1407         """
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))
1418
1419         msg = res[0]
1420         if "options" in msg:
1421             self.site_options = int(msg["options"][0])
1422
1423         if "interSiteTopologyGenerator" in msg:
1424             self.site_topo_generator = str(msg["interSiteTopologyGenerator"][0])
1425
1426         if "interSiteTopologyFailover" in msg:
1427             self.site_topo_failover = int(msg["interSiteTopologyFailover"][0])
1428
1429         self.load_all_dsa(samdb)
1430
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.
1435         """
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)
1442
1443         for msg in res:
1444             dnstr = str(msg.dn)
1445
1446             # already loaded
1447             if dnstr in self.dsa_table.keys():
1448                 continue
1449
1450             dsa = DirectoryServiceAgent(dnstr)
1451
1452             dsa.load_dsa(samdb)
1453
1454             # Assign this dsa to my dsa table
1455             # and index by dsa dn
1456             self.dsa_table[dnstr] = dsa
1457
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
1463
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
1467
1468         :return: None if DSA doesn't exist
1469         """
1470         if dnstr in self.dsa_table.keys():
1471             return self.dsa_table[dnstr]
1472         return None
1473
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.
1481         """
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
1486
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
1491
1492         if c_rep is None:
1493             raise Exception("Unable to find config NC replica for (%s)" %
1494                             mydsa.dsa_dnstr)
1495
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)
1500
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
1504         #
1505         #     Let s be the object such that s!lDAPDisplayName = nTDSDSA
1506         #     and classSchema in s!objectClass.
1507         #
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.
1511         #
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
1517
1518         unixnow = int(time.time())     # seconds since 1970
1519         ntnow = unix2nttime(unixnow) # double word number of 100 nanosecond
1520                                        # intervals since 1600s
1521
1522         for dsa in self.dsa_table.values():
1523             D_sort.append(dsa)
1524
1525         D_sort.sort(sort_dsa_by_guid)
1526
1527         # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1528         # if o!interSiteTopologyFailover is 0 or has no value.
1529         #
1530         # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1531         #       so it appears we have to turn f into the same interval
1532         #
1533         #       interSiteTopologyFailover (if set) appears to be in minutes
1534         #       so we'll need to convert to senconds and then 100 nanosecond
1535         #       intervals
1536         #
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
1542
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:
1547         #
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)
1552
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.
1561            #
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.
1566            #
1567            #     Else (Evidence of replication from current ITSG):
1568            #         Let i = j.
1569            #         Let t = c.timeLastSyncSuccess.
1570            #
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
1576
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
1583
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
1591
1592         # Compute a function that maintains the current ISTG if
1593         # it is alive, cycles through other candidates if not.
1594         #
1595         # Let k be the integer (i + ((current time - t) /
1596         #     o!interSiteTopologyFailover)) MOD |D|.
1597         #
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)
1601
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
1608
1609         # Update if we are the ISTG, otherwise return
1610         if d_dsa is not mydsa:
1611             return False
1612
1613         # Nothing to do
1614         if self.site_topo_generator == mydsa.dsa_dnstr:
1615             return True
1616
1617         self.site_topo_generator = mydsa.dsa_dnstr
1618
1619         # If readonly database then do not perform a
1620         # persistent update
1621         if ro:
1622             return True
1623
1624         # Perform update to the samdb
1625         ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1626
1627         m = ldb.Message()
1628         m.dn = ldb.Dn(samdb, ssdn)
1629
1630         m["interSiteTopologyGenerator"] = \
1631             ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
1632                                "interSiteTopologyGenerator")
1633         try:
1634             samdb.modify(m)
1635
1636         except ldb.LdbError, estr:
1637             raise Exception(
1638                 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1639                 (ssdn, estr))
1640         return True
1641
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
1648
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
1655
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
1662
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
1669
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
1676
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
1682
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
1693
1694
1695 class GraphNode(object):
1696     """A graph node describing a set of edges that should be directed to it.
1697
1698     Each edge is a connection for a particular naming context replica directed
1699     from another node in the forest to this node.
1700     """
1701
1702     def __init__(self, dsa_dnstr, max_node_edges):
1703         """Instantiate the graph node according to a DSA dn string
1704
1705         :param max_node_edges: maximum number of edges that should ever
1706             be directed to the node
1707         """
1708         self.max_edges = max_node_edges
1709         self.dsa_dnstr = dsa_dnstr
1710         self.edge_from = []
1711
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
1716
1717         for i, edge in enumerate(self.edge_from):
1718             text = text + "\n\tedge_from[%d]=%s" % (i, edge)
1719         return text
1720
1721     def add_edge_from(self, from_dsa_dnstr):
1722         """Add an edge from the dsa to our graph nodes edge from list
1723
1724         :param from_dsa_dnstr: the dsa that the edge emanates from
1725         """
1726         assert from_dsa_dnstr is not None
1727
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
1739
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
1746
1747         :param dsa: dsa with a dnstr equivalent to his graph node
1748         """
1749         for dnstr, connect in dsa.connect_table.items():
1750             self.add_edge_from(connect.from_dnstr)
1751
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.
1755         """
1756         for edge_dnstr in self.edge_from:
1757             connect = dsa.get_connection_by_from_dnstr(edge_dnstr)
1758
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:
1762             #
1763             #    c is a child of the DC's nTDSDSA object.
1764             #    c.objectCategory = nTDSConnection
1765             #
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".
1769             #
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
1775
1776             # if no such object exists then the KCC adds an object
1777             # c with the following attributes
1778             if exists:
1779                 return
1780
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
1785
1786             dsa.create_connection(opt, flags, None, edge_dnstr, None)
1787
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
1793
1794
1795 class Transport(object):
1796     """Class defines a Inter-site transport found under Sites
1797     """
1798
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 = []
1806
1807     def __str__(self):
1808         '''Debug dump string output of Transport object'''
1809
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
1817
1818         return text
1819
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.
1824         """
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)
1833
1834         except ldb.LdbError, (enum, estr):
1835             raise Exception("Unable to find Transport for (%s) - (%s)" %
1836                             (self.dnstr, estr))
1837
1838         msg = res[0]
1839         self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
1840                               msg["objectGUID"][0]))
1841
1842         if "options" in msg:
1843             self.options = int(msg["options"][0])
1844
1845         if "transportAddressAttribute" in msg:
1846             self.address_attr = str(msg["transportAddressAttribute"][0])
1847
1848         if "name" in msg:
1849             self.name = str(msg["name"][0])
1850
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)
1857
1858
1859 class RepsFromTo(object):
1860     """Class encapsulation of the NDR repsFromToBlob.
1861
1862     Removes the necessity of external code having to
1863     understand about other_info or manipulation of
1864     update flags.
1865     """
1866     def __init__(self, nc_dnstr=None, ndr_blob=None):
1867
1868         self.__dict__['to_be_deleted'] = False
1869         self.__dict__['nc_dnstr'] = nc_dnstr
1870         self.__dict__['update_flags'] = 0x0
1871
1872         # WARNING:
1873         #
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.
1879         #
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:
1885         #
1886         #   self.__dict__['ndr_blob'].ctr.other_info = \
1887         #        self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1888         #
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
1897
1898             self.__dict__['ndr_blob'].ctr.other_info = \
1899                 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1900
1901         else:
1902             self.__dict__['ndr_blob'] = ndr_blob
1903             self.__dict__['other_info'] = ndr_blob.ctr.other_info
1904
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
1911
1912     def __str__(self):
1913         '''Debug dump string output of class'''
1914
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
1918
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 + "]"
1942
1943         return text
1944
1945     def __setattr__(self, item, value):
1946
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' ]:
1951
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
1956
1957             setattr(self.__dict__['ndr_blob'].ctr, item, value)
1958
1959         elif item in ['dns_name1']:
1960             self.__dict__['dns_name1'] = value
1961
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']
1968
1969         elif item in ['dns_name2']:
1970             self.__dict__['dns_name2'] = value
1971
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']
1977
1978         elif item in ['nc_dnstr']:
1979             self.__dict__['nc_dnstr'] = value
1980
1981         elif item in ['to_be_deleted']:
1982             self.__dict__['to_be_deleted'] = value
1983
1984         elif item in ['version']:
1985             raise AttributeError, "Attempt to set readonly attribute %s" % item
1986         else:
1987             raise AttributeError, "Unknown attribute %s" % item
1988
1989         self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
1990
1991     def __getattr__(self, item):
1992         """Overload of RepsFromTo attribute retrieval.
1993
1994         Allows external code to ignore substructures within the blob
1995         """
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)
2001
2002         elif item in ['version']:
2003             return self.__dict__['ndr_blob'].version
2004
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
2010
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
2016
2017         elif item in ['to_be_deleted']:
2018             return self.__dict__['to_be_deleted']
2019
2020         elif item in ['nc_dnstr']:
2021             return self.__dict__['nc_dnstr']
2022
2023         elif item in ['update_flags']:
2024             return self.__dict__['update_flags']
2025
2026         raise AttributeError, "Unknwown attribute %s" % item
2027
2028     def is_modified(self):
2029         return (self.update_flags != 0x0)
2030
2031     def set_unmodified(self):
2032         self.__dict__['update_flags'] = 0x0
2033
2034
2035 class SiteLink(object):
2036     """Class defines a site link found under sites
2037     """
2038
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 = []
2047
2048     def __str__(self):
2049         '''Debug dump string output of Transport object'''
2050
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
2056
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
2062
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 + "]"
2072
2073         for dnstr in self.site_list:
2074             text = text + "\n\tsite_list=%s" % dnstr
2075         return text
2076
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.
2081         """
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)
2091
2092         except ldb.LdbError, (enum, estr):
2093             raise Exception("Unable to find SiteLink for (%s) - (%s)" %
2094                             (self.dnstr, estr))
2095
2096         msg = res[0]
2097
2098         if "options" in msg:
2099             self.options = int(msg["options"][0])
2100
2101         if "systemFlags" in msg:
2102             self.system_flags = int(msg["systemFlags"][0])
2103
2104         if "cost" in msg:
2105             self.cost = int(msg["cost"][0])
2106
2107         if "replInterval" in msg:
2108             self.interval = int(msg["replInterval"][0])
2109
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)
2116
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
2120         """
2121         if site1_dnstr in self.site_list and site2_dnstr in self.site_list:
2122             return True
2123         return False
2124
2125
2126 class VertexColor(object):
2127     (unknown, white, black, red) = range(0, 4)
2128
2129
2130 class Vertex(object):
2131     """Class encapsulation of a Site Vertex in the
2132     intersite topology replication algorithm
2133     """
2134     def __init__(self, site, part):
2135         self.site = site
2136         self.part = part
2137         self.color = VertexColor.unknown
2138
2139     def color_vertex(self):
2140         """Color each vertex to indicate which kind of NC
2141         replica it contains
2142         """
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
2150
2151         # set to minimum (no replica)
2152         self.color = VertexColor.white
2153
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
2158
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
2166
2167     def is_red(self):
2168         assert(self.color != VertexColor.unknown)
2169         return (self.color == VertexColor.red)
2170
2171     def is_black(self):
2172         assert(self.color != VertexColor.unknown)
2173         return (self.color == VertexColor.black)
2174
2175     def is_white(self):
2176         assert(self.color != VertexColor.unknown)
2177         return (self.color == VertexColor.white)
2178
2179 ##################################################
2180 # Global Functions
2181 ##################################################
2182 def sort_dsa_by_guid(dsa1, dsa2):
2183     return cmp(dsa1.dsa_guid, dsa2.dsa_guid)