samba-tool domain join: Add --quite and --verbose
[nivanova/samba-autobuild/.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_BEHAVIOR_WIN2008)
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(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, 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             self.transport_dnstr = tdnstr
936             self.transport_guid = \
937                 misc.GUID(samdb.schema_format_value("objectGUID",
938                                                     msg["objectGUID"][0]))
939         assert self.transport_dnstr is not None
940         assert self.transport_guid is not None
941
942     def commit_deleted(self, samdb, ro=False):
943         """Local helper routine for commit_connections() which
944         handles committed connections that are to be deleted from
945         the database database
946         """
947         assert self.to_be_deleted
948         self.to_be_deleted = False
949
950         # No database modification requested
951         if ro:
952             return
953
954         try:
955             samdb.delete(self.dnstr)
956         except ldb.LdbError, (enum, estr):
957             raise Exception("Could not delete nTDSConnection for (%s) - (%s)" %
958                             (self.dnstr, estr))
959
960     def commit_added(self, samdb, ro=False):
961         """Local helper routine for commit_connections() which
962         handles committed connections that are to be added to the
963         database
964         """
965         assert self.to_be_added
966         self.to_be_added = False
967
968         # No database modification requested
969         if ro:
970             return
971
972         # First verify we don't have this entry to ensure nothing
973         # is programatically amiss
974         found = False
975         try:
976             msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
977             if len(msg) != 0:
978                 found = True
979
980         except ldb.LdbError, (enum, estr):
981             if enum != ldb.ERR_NO_SUCH_OBJECT:
982                 raise Exception("Unable to search for (%s) - (%s)" %
983                                 (self.dnstr, estr))
984         if found:
985             raise Exception("nTDSConnection for (%s) already exists!" %
986                             self.dnstr)
987
988         if self.enabled:
989             enablestr = "TRUE"
990         else:
991             enablestr = "FALSE"
992
993         # Prepare a message for adding to the samdb
994         m = ldb.Message()
995         m.dn = ldb.Dn(samdb, self.dnstr)
996
997         m["objectClass"] = \
998             ldb.MessageElement("nTDSConnection", ldb.FLAG_MOD_ADD,
999                                "objectClass")
1000         m["showInAdvancedViewOnly"] = \
1001             ldb.MessageElement("TRUE", ldb.FLAG_MOD_ADD,
1002                                "showInAdvancedViewOnly")
1003         m["enabledConnection"] = \
1004             ldb.MessageElement(enablestr, ldb.FLAG_MOD_ADD, "enabledConnection")
1005         m["fromServer"] = \
1006             ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_ADD, "fromServer")
1007         m["options"] = \
1008             ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options")
1009         m["systemFlags"] = \
1010             ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_ADD,
1011                                "systemFlags")
1012
1013         if self.transport_dnstr is not None:
1014             m["transportType"] = \
1015                 ldb.MessageElement(str(self.transport_dnstr), ldb.FLAG_MOD_ADD,
1016                                    "transportType")
1017
1018         if self.schedule is not None:
1019             m["schedule"] = \
1020                 ldb.MessageElement(ndr_pack(self.schedule),
1021                                    ldb.FLAG_MOD_ADD, "schedule")
1022         try:
1023             samdb.add(m)
1024         except ldb.LdbError, (enum, estr):
1025             raise Exception("Could not add nTDSConnection for (%s) - (%s)" %
1026                             (self.dnstr, estr))
1027
1028     def commit_modified(self, samdb, ro=False):
1029         """Local helper routine for commit_connections() which
1030         handles committed connections that are to be modified to the
1031         database
1032         """
1033         assert self.to_be_modified
1034         self.to_be_modified = False
1035
1036         # No database modification requested
1037         if ro:
1038             return
1039
1040         # First verify we have this entry to ensure nothing
1041         # is programatically amiss
1042         try:
1043             msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1044             found = True
1045
1046         except ldb.LdbError, (enum, estr):
1047             if enum == ldb.ERR_NO_SUCH_OBJECT:
1048                 found = False
1049             else:
1050                 raise Exception("Unable to search for (%s) - (%s)" %
1051                                 (self.dnstr, estr))
1052         if not found:
1053             raise Exception("nTDSConnection for (%s) doesn't exist!" %
1054                             self.dnstr)
1055
1056         if self.enabled:
1057             enablestr = "TRUE"
1058         else:
1059             enablestr = "FALSE"
1060
1061         # Prepare a message for modifying the samdb
1062         m = ldb.Message()
1063         m.dn = ldb.Dn(samdb, self.dnstr)
1064
1065         m["enabledConnection"] = \
1066             ldb.MessageElement(enablestr, ldb.FLAG_MOD_REPLACE,
1067                                "enabledConnection")
1068         m["fromServer"] = \
1069             ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_REPLACE,
1070                                "fromServer")
1071         m["options"] = \
1072             ldb.MessageElement(str(self.options), ldb.FLAG_MOD_REPLACE,
1073                                "options")
1074         m["systemFlags"] = \
1075             ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_REPLACE,
1076                                "systemFlags")
1077
1078         if self.transport_dnstr is not None:
1079             m["transportType"] = \
1080                 ldb.MessageElement(str(self.transport_dnstr),
1081                                    ldb.FLAG_MOD_REPLACE, "transportType")
1082         else:
1083             m["transportType"] = \
1084                 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "transportType")
1085
1086         if self.schedule is not None:
1087             m["schedule"] = \
1088                 ldb.MessageElement(ndr_pack(self.schedule),
1089                                    ldb.FLAG_MOD_REPLACE, "schedule")
1090         else:
1091             m["schedule"] = \
1092                 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "schedule")
1093         try:
1094             samdb.modify(m)
1095         except ldb.LdbError, (enum, estr):
1096             raise Exception("Could not modify nTDSConnection for (%s) - (%s)" %
1097                             (self.dnstr, estr))
1098
1099     def set_modified(self, truefalse):
1100         self.to_be_modified = truefalse
1101
1102     def set_added(self, truefalse):
1103         self.to_be_added = truefalse
1104
1105     def set_deleted(self, truefalse):
1106         self.to_be_deleted = truefalse
1107
1108     def is_schedule_minimum_once_per_week(self):
1109         """Returns True if our schedule includes at least one
1110         replication interval within the week.  False otherwise
1111         """
1112         if self.schedule is None or self.schedule.dataArray[0] is None:
1113             return False
1114
1115         for slot in self.schedule.dataArray[0].slots:
1116            if (slot & 0x0F) != 0x0:
1117                return True
1118         return False
1119
1120     def is_equivalent_schedule(self, sched):
1121         """Returns True if our schedule is equivalent to the input
1122         comparison schedule.
1123
1124         :param shed: schedule to compare to
1125         """
1126         if self.schedule is not None:
1127             if sched is None:
1128                return False
1129         elif sched is None:
1130             return True
1131
1132         if (self.schedule.size != sched.size or
1133             self.schedule.bandwidth != sched.bandwidth or
1134             self.schedule.numberOfSchedules != sched.numberOfSchedules):
1135             return False
1136
1137         for i, header in enumerate(self.schedule.headerArray):
1138
1139             if self.schedule.headerArray[i].type != sched.headerArray[i].type:
1140                 return False
1141
1142             if self.schedule.headerArray[i].offset != \
1143                sched.headerArray[i].offset:
1144                 return False
1145
1146             for a, b in zip(self.schedule.dataArray[i].slots,
1147                             sched.dataArray[i].slots):
1148                 if a != b:
1149                     return False
1150         return True
1151
1152     def convert_schedule_to_repltimes(self):
1153         """Convert NTDS Connection schedule to replTime schedule.
1154
1155         NTDS Connection schedule slots are double the size of
1156         the replTime slots but the top portion of the NTDS
1157         Connection schedule slot (4 most significant bits in
1158         uchar) are unused.  The 4 least significant bits have
1159         the same (15 minute interval) bit positions as replTimes.
1160         We thus pack two elements of the NTDS Connection schedule
1161         slots into one element of the replTimes slot
1162         If no schedule appears in NTDS Connection then a default
1163         of 0x11 is set in each replTimes slot as per behaviour
1164         noted in a Windows DC.  That default would cause replication
1165         within the last 15 minutes of each hour.
1166         """
1167         times = [0x11] * 84
1168
1169         for i, slot in enumerate(times):
1170             if self.schedule is not None and \
1171                self.schedule.dataArray[0] is not None:
1172                 slot = (self.schedule.dataArray[0].slots[i*2] & 0xF) << 4 | \
1173                        (self.schedule.dataArray[0].slots[i*2] & 0xF)
1174         return times
1175
1176     def is_rodc_topology(self):
1177         """Returns True if NTDS Connection specifies RODC
1178         topology only
1179         """
1180         if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
1181             return False
1182         return True
1183
1184     def is_generated(self):
1185         """Returns True if NTDS Connection was generated by the
1186         KCC topology algorithm as opposed to set by the administrator
1187         """
1188         if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0:
1189             return False
1190         return True
1191
1192     def is_override_notify_default(self):
1193         """Returns True if NTDS Connection should override notify default
1194         """
1195         if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0:
1196             return False
1197         return True
1198
1199     def is_use_notify(self):
1200         """Returns True if NTDS Connection should use notify
1201         """
1202         if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0:
1203             return False
1204         return True
1205
1206     def is_twoway_sync(self):
1207         """Returns True if NTDS Connection should use twoway sync
1208         """
1209         if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0:
1210             return False
1211         return True
1212
1213     def is_intersite_compression_disabled(self):
1214         """Returns True if NTDS Connection intersite compression
1215         is disabled
1216         """
1217         if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0:
1218             return False
1219         return True
1220
1221     def is_user_owned_schedule(self):
1222         """Returns True if NTDS Connection has a user owned schedule
1223         """
1224         if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0:
1225             return False
1226         return True
1227
1228     def is_enabled(self):
1229         """Returns True if NTDS Connection is enabled
1230         """
1231         return self.enabled
1232
1233     def get_from_dnstr(self):
1234         '''Return fromServer dn string attribute'''
1235         return self.from_dnstr
1236
1237
1238 class Partition(NamingContext):
1239     """A naming context discovered thru Partitions DN of the config schema.
1240
1241     This is a more specific form of NamingContext class (inheriting from that
1242     class) and it identifies unique attributes enumerated in the Partitions
1243     such as which nTDSDSAs are cross referenced for replicas
1244     """
1245     def __init__(self, partstr):
1246         self.partstr = partstr
1247         self.enabled = True
1248         self.system_flags = 0
1249         self.rw_location_list = []
1250         self.ro_location_list = []
1251
1252         # We don't have enough info to properly
1253         # fill in the naming context yet.  We'll get that
1254         # fully set up with load_partition().
1255         NamingContext.__init__(self, None)
1256
1257
1258     def load_partition(self, samdb):
1259         """Given a Partition class object that has been initialized with its
1260         partition dn string, load the partition from the sam database, identify
1261         the type of the partition (schema, domain, etc) and record the list of
1262         nTDSDSAs that appear in the cross reference attributes
1263         msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations.
1264
1265         :param samdb: sam database to load partition from
1266         """
1267         attrs = [ "nCName",
1268                   "Enabled",
1269                   "systemFlags",
1270                   "msDS-NC-Replica-Locations",
1271                   "msDS-NC-RO-Replica-Locations" ]
1272         try:
1273             res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE,
1274                                attrs=attrs)
1275
1276         except ldb.LdbError, (enum, estr):
1277             raise Exception("Unable to find partition for (%s) - (%s)" % (
1278                             self.partstr, estr))
1279
1280         msg = res[0]
1281         for k in msg.keys():
1282             if k == "dn":
1283                 continue
1284
1285             if k == "Enabled":
1286                 if msg[k][0].upper().lstrip().rstrip() == "TRUE":
1287                     self.enabled = True
1288                 else:
1289                     self.enabled = False
1290                 continue
1291
1292             if k == "systemFlags":
1293                 self.system_flags = int(msg[k][0])
1294                 continue
1295
1296             for value in msg[k]:
1297                 dsdn = dsdb_Dn(samdb, value)
1298                 dnstr = str(dsdn.dn)
1299
1300                 if k == "nCName":
1301                     self.nc_dnstr = dnstr
1302                     continue
1303
1304                 if k == "msDS-NC-Replica-Locations":
1305                     self.rw_location_list.append(dnstr)
1306                     continue
1307
1308                 if k == "msDS-NC-RO-Replica-Locations":
1309                     self.ro_location_list.append(dnstr)
1310                     continue
1311
1312         # Now identify what type of NC this partition
1313         # enumerated
1314         self.identify_by_basedn(samdb)
1315
1316     def is_enabled(self):
1317         """Returns True if partition is enabled
1318         """
1319         return self.is_enabled
1320
1321     def is_foreign(self):
1322         """Returns True if this is not an Active Directory NC in our
1323         forest but is instead something else (e.g. a foreign NC)
1324         """
1325         if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0:
1326             return True
1327         else:
1328             return False
1329
1330     def should_be_present(self, target_dsa):
1331         """Tests whether this partition should have an NC replica
1332         on the target dsa.  This method returns a tuple of
1333         needed=True/False, ro=True/False, partial=True/False
1334
1335         :param target_dsa: should NC be present on target dsa
1336         """
1337         needed = False
1338         ro = False
1339         partial = False
1340
1341         # If this is the config, schema, or default
1342         # domain NC for the target dsa then it should
1343         # be present
1344         if self.nc_type == NCType.config or \
1345            self.nc_type == NCType.schema or \
1346            (self.nc_type == NCType.domain and
1347             self.nc_dnstr == target_dsa.default_dnstr):
1348             needed = True
1349
1350         # A writable replica of an application NC should be present
1351         # if there a cross reference to the target DSA exists.  Depending
1352         # on whether the DSA is ro we examine which type of cross reference
1353         # to look for (msDS-NC-Replica-Locations or
1354         # msDS-NC-RO-Replica-Locations
1355         if self.nc_type == NCType.application:
1356             if target_dsa.is_ro():
1357                if target_dsa.dsa_dnstr in self.ro_location_list:
1358                    needed = True
1359             else:
1360                if target_dsa.dsa_dnstr in self.rw_location_list:
1361                    needed = True
1362
1363         # If the target dsa is a gc then a partial replica of a
1364         # domain NC (other than the DSAs default domain) should exist
1365         # if there is also a cross reference for the DSA
1366         if target_dsa.is_gc() and \
1367            self.nc_type == NCType.domain and \
1368            self.nc_dnstr != target_dsa.default_dnstr and \
1369            (target_dsa.dsa_dnstr in self.ro_location_list or
1370             target_dsa.dsa_dnstr in self.rw_location_list):
1371             needed = True
1372             partial = True
1373
1374         # partial NCs are always readonly
1375         if needed and (target_dsa.is_ro() or partial):
1376             ro = True
1377
1378         return needed, ro, partial
1379
1380     def __str__(self):
1381         '''Debug dump string output of class'''
1382         text = "%s" % NamingContext.__str__(self)
1383         text = text + "\n\tpartdn=%s" % self.partstr
1384         for k in self.rw_location_list:
1385             text = text + "\n\tmsDS-NC-Replica-Locations=%s" % k
1386         for k in self.ro_location_list:
1387             text = text + "\n\tmsDS-NC-RO-Replica-Locations=%s" % k
1388         return text
1389
1390
1391 class Site(object):
1392     """An individual site object discovered thru the configuration
1393     naming context.  Contains all DSAs that exist within the site
1394     """
1395     def __init__(self, site_dnstr):
1396         self.site_dnstr = site_dnstr
1397         self.site_options = 0
1398         self.site_topo_generator = None
1399         self.site_topo_failover = 0  # appears to be in minutes
1400         self.dsa_table = {}
1401
1402     def load_site(self, samdb):
1403         """Loads the NTDS Site Settions options attribute for the site
1404         as well as querying and loading all DSAs that appear within
1405         the site.
1406         """
1407         ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1408         attrs = ["options",
1409                  "interSiteTopologyFailover",
1410                  "interSiteTopologyGenerator"]
1411         try:
1412             res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE,
1413                                attrs=attrs)
1414         except ldb.LdbError, (enum, estr):
1415             raise Exception("Unable to find site settings for (%s) - (%s)" %
1416                             (ssdn, estr))
1417
1418         msg = res[0]
1419         if "options" in msg:
1420             self.site_options = int(msg["options"][0])
1421
1422         if "interSiteTopologyGenerator" in msg:
1423             self.site_topo_generator = str(msg["interSiteTopologyGenerator"][0])
1424
1425         if "interSiteTopologyFailover" in msg:
1426             self.site_topo_failover = int(msg["interSiteTopologyFailover"][0])
1427
1428         self.load_all_dsa(samdb)
1429
1430     def load_all_dsa(self, samdb):
1431         """Discover all nTDSDSA thru the sites entry and
1432         instantiate and load the DSAs.  Each dsa is inserted
1433         into the dsa_table by dn string.
1434         """
1435         try:
1436             res = samdb.search(self.site_dnstr,
1437                                scope=ldb.SCOPE_SUBTREE,
1438                                expression="(objectClass=nTDSDSA)")
1439         except ldb.LdbError, (enum, estr):
1440             raise Exception("Unable to find nTDSDSAs - (%s)" % estr)
1441
1442         for msg in res:
1443             dnstr = str(msg.dn)
1444
1445             # already loaded
1446             if dnstr in self.dsa_table.keys():
1447                 continue
1448
1449             dsa = DirectoryServiceAgent(dnstr)
1450
1451             dsa.load_dsa(samdb)
1452
1453             # Assign this dsa to my dsa table
1454             # and index by dsa dn
1455             self.dsa_table[dnstr] = dsa
1456
1457     def get_dsa_by_guidstr(self, guidstr):
1458         for dsa in self.dsa_table.values():
1459             if str(dsa.dsa_guid) == guidstr:
1460                 return dsa
1461         return None
1462
1463     def get_dsa(self, dnstr):
1464         """Return a previously loaded DSA object by consulting
1465         the sites dsa_table for the provided DSA dn string
1466
1467         :return: None if DSA doesn't exist
1468         """
1469         if dnstr in self.dsa_table.keys():
1470             return self.dsa_table[dnstr]
1471         return None
1472
1473     def select_istg(self, samdb, mydsa, ro):
1474         """Determine if my DC should be an intersite topology
1475         generator.  If my DC is the istg and is both a writeable
1476         DC and the database is opened in write mode then we perform
1477         an originating update to set the interSiteTopologyGenerator
1478         attribute in the NTDS Site Settings object.  An RODC always
1479         acts as an ISTG for itself.
1480         """
1481         # The KCC on an RODC always acts as an ISTG for itself
1482         if mydsa.dsa_is_ro:
1483             mydsa.dsa_is_istg = True
1484             return True
1485
1486         # Find configuration NC replica for my DSA
1487         for c_rep in mydsa.current_rep_table.values():
1488             if c_rep.is_config():
1489                 break
1490
1491         if c_rep is None:
1492             raise Exception("Unable to find config NC replica for (%s)" %
1493                             mydsa.dsa_dnstr)
1494
1495         # Load repsFrom if not already loaded so we can get the current
1496         # state of the config replica and whether we are getting updates
1497         # from the istg
1498         c_rep.load_repsFrom(samdb)
1499
1500         # From MS-Tech ISTG selection:
1501         #     First, the KCC on a writable DC determines whether it acts
1502         #     as an ISTG for its site
1503         #
1504         #     Let s be the object such that s!lDAPDisplayName = nTDSDSA
1505         #     and classSchema in s!objectClass.
1506         #
1507         #     Let D be the sequence of objects o in the site of the local
1508         #     DC such that o!objectCategory = s. D is sorted in ascending
1509         #     order by objectGUID.
1510         #
1511         # Which is a fancy way of saying "sort all the nTDSDSA objects
1512         # in the site by guid in ascending order".   Place sorted list
1513         # in D_sort[]
1514         D_sort = []
1515         d_dsa = None
1516
1517         unixnow = int(time.time())     # seconds since 1970
1518         ntnow = unix2nttime(unixnow) # double word number of 100 nanosecond
1519                                        # intervals since 1600s
1520
1521         for dsa in self.dsa_table.values():
1522             D_sort.append(dsa)
1523
1524         D_sort.sort(sort_dsa_by_guid)
1525
1526         # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1527         # if o!interSiteTopologyFailover is 0 or has no value.
1528         #
1529         # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1530         #       so it appears we have to turn f into the same interval
1531         #
1532         #       interSiteTopologyFailover (if set) appears to be in minutes
1533         #       so we'll need to convert to senconds and then 100 nanosecond
1534         #       intervals
1535         #
1536         #       10,000,000 is number of 100 nanosecond intervals in a second
1537         if self.site_topo_failover == 0:
1538             f = 2 * 60 * 60 * 10000000
1539         else:
1540             f = self.site_topo_failover * 60 * 10000000
1541
1542         # From MS-Tech ISTG selection:
1543         #     If o != NULL and o!interSiteTopologyGenerator is not the
1544         #     nTDSDSA object for the local DC and
1545         #     o!interSiteTopologyGenerator is an element dj of sequence D:
1546         #
1547         if self.site_topo_generator is not None and \
1548            self.site_topo_generator in self.dsa_table.keys():
1549             d_dsa = self.dsa_table[self.site_topo_generator]
1550             j_idx = D_sort.index(d_dsa)
1551
1552         if d_dsa is not None and d_dsa is not mydsa:
1553            # From MS-Tech ISTG selection:
1554            #     Let c be the cursor in the replUpToDateVector variable
1555            #     associated with the NC replica of the config NC such
1556            #     that c.uuidDsa = dj!invocationId. If no such c exists
1557            #     (No evidence of replication from current ITSG):
1558            #         Let i = j.
1559            #         Let t = 0.
1560            #
1561            #     Else if the current time < c.timeLastSyncSuccess - f
1562            #     (Evidence of time sync problem on current ISTG):
1563            #         Let i = 0.
1564            #         Let t = 0.
1565            #
1566            #     Else (Evidence of replication from current ITSG):
1567            #         Let i = j.
1568            #         Let t = c.timeLastSyncSuccess.
1569            #
1570            # last_success appears to be a double word containing
1571            #     number of 100 nanosecond intervals since the 1600s
1572            if d_dsa.dsa_ivid != c_rep.source_dsa_invocation_id:
1573                i_idx = j_idx
1574                t_time = 0
1575
1576            elif ntnow < (c_rep.last_success - f):
1577                i_idx = 0
1578                t_time = 0
1579            else:
1580                i_idx = j_idx
1581                t_time = c_rep.last_success
1582
1583         # Otherwise (Nominate local DC as ISTG):
1584         #     Let i be the integer such that di is the nTDSDSA
1585         #         object for the local DC.
1586         #     Let t = the current time.
1587         else:
1588             i_idx = D_sort.index(mydsa)
1589             t_time = ntnow
1590
1591         # Compute a function that maintains the current ISTG if
1592         # it is alive, cycles through other candidates if not.
1593         #
1594         # Let k be the integer (i + ((current time - t) /
1595         #     o!interSiteTopologyFailover)) MOD |D|.
1596         #
1597         # Note: We don't want to divide by zero here so they must
1598         #       have meant "f" instead of "o!interSiteTopologyFailover"
1599         k_idx = (i_idx + ((ntnow - t_time) / f)) % len(D_sort)
1600
1601         # The local writable DC acts as an ISTG for its site if and
1602         # only if dk is the nTDSDSA object for the local DC. If the
1603         # local DC does not act as an ISTG, the KCC skips the
1604         # remainder of this task.
1605         d_dsa = D_sort[k_idx]
1606         d_dsa.dsa_is_istg = True
1607
1608         # Update if we are the ISTG, otherwise return
1609         if d_dsa is not mydsa:
1610             return False
1611
1612         # Nothing to do
1613         if self.site_topo_generator == mydsa.dsa_dnstr:
1614             return True
1615
1616         self.site_topo_generator = mydsa.dsa_dnstr
1617
1618         # If readonly database then do not perform a
1619         # persistent update
1620         if ro:
1621             return True
1622
1623         # Perform update to the samdb
1624         ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1625
1626         m = ldb.Message()
1627         m.dn = ldb.Dn(samdb, ssdn)
1628
1629         m["interSiteTopologyGenerator"] = \
1630             ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
1631                                "interSiteTopologyGenerator")
1632         try:
1633             samdb.modify(m)
1634
1635         except ldb.LdbError, estr:
1636             raise Exception(
1637                 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1638                 (ssdn, estr))
1639         return True
1640
1641     def is_intrasite_topology_disabled(self):
1642         '''Returns True if intra-site topology is disabled for site'''
1643         if (self.site_options &
1644             dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0:
1645             return True
1646         return False
1647
1648     def is_intersite_topology_disabled(self):
1649         '''Returns True if inter-site topology is disabled for site'''
1650         if (self.site_options &
1651             dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED) != 0:
1652             return True
1653         return False
1654
1655     def is_random_bridgehead_disabled(self):
1656         '''Returns True if selection of random bridgehead is disabled'''
1657         if (self.site_options &
1658             dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0:
1659             return True
1660         return False
1661
1662     def is_detect_stale_disabled(self):
1663         '''Returns True if detect stale is disabled for site'''
1664         if (self.site_options &
1665             dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0:
1666             return True
1667         return False
1668
1669     def is_cleanup_ntdsconn_disabled(self):
1670         '''Returns True if NTDS Connection cleanup is disabled for site'''
1671         if (self.site_options &
1672             dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0:
1673             return True
1674         return False
1675
1676     def same_site(self, dsa):
1677        '''Return True if dsa is in this site'''
1678        if self.get_dsa(dsa.dsa_dnstr):
1679            return True
1680        return False
1681
1682     def __str__(self):
1683         '''Debug dump string output of class'''
1684         text = "%s:" % self.__class__.__name__
1685         text = text + "\n\tdn=%s"             % self.site_dnstr
1686         text = text + "\n\toptions=0x%X"      % self.site_options
1687         text = text + "\n\ttopo_generator=%s" % self.site_topo_generator
1688         text = text + "\n\ttopo_failover=%d"  % self.site_topo_failover
1689         for key, dsa in self.dsa_table.items():
1690             text = text + "\n%s" % dsa
1691         return text
1692
1693
1694 class GraphNode(object):
1695     """A graph node describing a set of edges that should be directed to it.
1696
1697     Each edge is a connection for a particular naming context replica directed
1698     from another node in the forest to this node.
1699     """
1700
1701     def __init__(self, dsa_dnstr, max_node_edges):
1702         """Instantiate the graph node according to a DSA dn string
1703
1704         :param max_node_edges: maximum number of edges that should ever
1705             be directed to the node
1706         """
1707         self.max_edges = max_node_edges
1708         self.dsa_dnstr = dsa_dnstr
1709         self.edge_from = []
1710
1711     def __str__(self):
1712         text = "%s:" % self.__class__.__name__
1713         text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
1714         text = text + "\n\tmax_edges=%d" % self.max_edges
1715
1716         for i, edge in enumerate(self.edge_from):
1717             text = text + "\n\tedge_from[%d]=%s" % (i, edge)
1718         return text
1719
1720     def add_edge_from(self, from_dsa_dnstr):
1721         """Add an edge from the dsa to our graph nodes edge from list
1722
1723         :param from_dsa_dnstr: the dsa that the edge emanates from
1724         """
1725         assert from_dsa_dnstr is not None
1726
1727         # No edges from myself to myself
1728         if from_dsa_dnstr == self.dsa_dnstr:
1729             return False
1730         # Only one edge from a particular node
1731         if from_dsa_dnstr in self.edge_from:
1732             return False
1733         # Not too many edges
1734         if len(self.edge_from) >= self.max_edges:
1735             return False
1736         self.edge_from.append(from_dsa_dnstr)
1737         return True
1738
1739     def add_edges_from_connections(self, dsa):
1740         """For each nTDSConnection object associated with a particular
1741         DSA, we test if it implies an edge to this graph node (i.e.
1742         the "fromServer" attribute).  If it does then we add an
1743         edge from the server unless we are over the max edges for this
1744         graph node
1745
1746         :param dsa: dsa with a dnstr equivalent to his graph node
1747         """
1748         for dnstr, connect in dsa.connect_table.items():
1749             self.add_edge_from(connect.from_dnstr)
1750
1751     def add_connections_from_edges(self, dsa):
1752         """For each edge directed to this graph node, ensure there
1753            is a corresponding nTDSConnection object in the dsa.
1754         """
1755         for edge_dnstr in self.edge_from:
1756             connect = dsa.get_connection_by_from_dnstr(edge_dnstr)
1757
1758             # For each edge directed to the NC replica that
1759             # "should be present" on the local DC, the KCC determines
1760             # whether an object c exists such that:
1761             #
1762             #    c is a child of the DC's nTDSDSA object.
1763             #    c.objectCategory = nTDSConnection
1764             #
1765             # Given the NC replica ri from which the edge is directed,
1766             #    c.fromServer is the dsname of the nTDSDSA object of
1767             #    the DC on which ri "is present".
1768             #
1769             #    c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1770             if connect and not connect.is_rodc_topology():
1771                 exists = True
1772             else:
1773                 exists = False
1774
1775             # if no such object exists then the KCC adds an object
1776             # c with the following attributes
1777             if exists:
1778                 return
1779
1780             # Generate a new dnstr for this nTDSConnection
1781             opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1782             flags = dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME + \
1783                      dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE
1784
1785             dsa.create_connection(opt, flags, None, edge_dnstr, None)
1786
1787     def has_sufficient_edges(self):
1788         '''Return True if we have met the maximum "from edges" criteria'''
1789         if len(self.edge_from) >= self.max_edges:
1790             return True
1791         return False
1792
1793
1794 class Transport(object):
1795     """Class defines a Inter-site transport found under Sites
1796     """
1797
1798     def __init__(self, dnstr):
1799         self.dnstr = dnstr
1800         self.options = 0
1801         self.guid = None
1802         self.name = None
1803         self.address_attr = None
1804         self.bridgehead_list = []
1805
1806     def __str__(self):
1807         '''Debug dump string output of Transport object'''
1808
1809         text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
1810         text = text + "\n\tguid=%s" % str(self.guid)
1811         text = text + "\n\toptions=%d" % self.options
1812         text = text + "\n\taddress_attr=%s" % self.address_attr
1813         text = text + "\n\tname=%s" % self.name
1814         for dnstr in self.bridgehead_list:
1815             text = text + "\n\tbridgehead_list=%s" % dnstr
1816
1817         return text
1818
1819     def load_transport(self, samdb):
1820         """Given a Transport object with an prior initialization
1821         for the object's DN, search for the DN and load attributes
1822         from the samdb.
1823         """
1824         attrs = [ "objectGUID",
1825                   "options",
1826                   "name",
1827                   "bridgeheadServerListBL",
1828                   "transportAddressAttribute" ]
1829         try:
1830             res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
1831                                attrs=attrs)
1832
1833         except ldb.LdbError, (enum, estr):
1834             raise Exception("Unable to find Transport for (%s) - (%s)" %
1835                             (self.dnstr, estr))
1836
1837         msg = res[0]
1838         self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
1839                               msg["objectGUID"][0]))
1840
1841         if "options" in msg:
1842             self.options = int(msg["options"][0])
1843
1844         if "transportAddressAttribute" in msg:
1845             self.address_attr = str(msg["transportAddressAttribute"][0])
1846
1847         if "name" in msg:
1848             self.name = str(msg["name"][0])
1849
1850         if "bridgeheadServerListBL" in msg:
1851             for value in msg["bridgeheadServerListBL"]:
1852                 dsdn = dsdb_Dn(samdb, value)
1853                 dnstr = str(dsdn.dn)
1854                 if dnstr not in self.bridgehead_list:
1855                     self.bridgehead_list.append(dnstr)
1856
1857
1858 class RepsFromTo(object):
1859     """Class encapsulation of the NDR repsFromToBlob.
1860
1861     Removes the necessity of external code having to
1862     understand about other_info or manipulation of
1863     update flags.
1864     """
1865     def __init__(self, nc_dnstr=None, ndr_blob=None):
1866
1867         self.__dict__['to_be_deleted'] = False
1868         self.__dict__['nc_dnstr'] = nc_dnstr
1869         self.__dict__['update_flags'] = 0x0
1870
1871         # WARNING:
1872         #
1873         # There is a very subtle bug here with python
1874         # and our NDR code.  If you assign directly to
1875         # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1876         # then a proper python GC reference count is not
1877         # maintained.
1878         #
1879         # To work around this we maintain an internal
1880         # reference to "dns_name(x)" and "other_info" elements
1881         # of repsFromToBlob.  This internal reference
1882         # is hidden within this class but it is why you
1883         # see statements like this below:
1884         #
1885         #   self.__dict__['ndr_blob'].ctr.other_info = \
1886         #        self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1887         #
1888         # That would appear to be a redundant assignment but
1889         # it is necessary to hold a proper python GC reference
1890         # count.
1891         if ndr_blob is None:
1892             self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
1893             self.__dict__['ndr_blob'].version = 0x1
1894             self.__dict__['dns_name1'] = None
1895             self.__dict__['dns_name2'] = None
1896
1897             self.__dict__['ndr_blob'].ctr.other_info = \
1898                 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1899
1900         else:
1901             self.__dict__['ndr_blob'] = ndr_blob
1902             self.__dict__['other_info'] = ndr_blob.ctr.other_info
1903
1904             if ndr_blob.version == 0x1:
1905                 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
1906                 self.__dict__['dns_name2'] = None
1907             else:
1908                 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
1909                 self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
1910
1911     def __str__(self):
1912         '''Debug dump string output of class'''
1913
1914         text = "%s:" % self.__class__.__name__
1915         text = text + "\n\tdnstr=%s" % self.nc_dnstr
1916         text = text + "\n\tupdate_flags=0x%X" % self.update_flags
1917
1918         text = text + "\n\tversion=%d" % self.version
1919         text = text + "\n\tsource_dsa_obj_guid=%s" % \
1920                str(self.source_dsa_obj_guid)
1921         text = text + "\n\tsource_dsa_invocation_id=%s" % \
1922                str(self.source_dsa_invocation_id)
1923         text = text + "\n\ttransport_guid=%s" % \
1924                str(self.transport_guid)
1925         text = text + "\n\treplica_flags=0x%X" % \
1926                self.replica_flags
1927         text = text + "\n\tconsecutive_sync_failures=%d" % \
1928                self.consecutive_sync_failures
1929         text = text + "\n\tlast_success=%s" % \
1930                self.last_success
1931         text = text + "\n\tlast_attempt=%s" % \
1932                self.last_attempt
1933         text = text + "\n\tdns_name1=%s" % \
1934                str(self.dns_name1)
1935         text = text + "\n\tdns_name2=%s" % \
1936                str(self.dns_name2)
1937         text = text + "\n\tschedule[ "
1938         for slot in self.schedule:
1939             text = text + "0x%X " % slot
1940         text = text + "]"
1941
1942         return text
1943
1944     def __setattr__(self, item, value):
1945
1946         if item in [ 'schedule', 'replica_flags', 'transport_guid',
1947                      'source_dsa_obj_guid', 'source_dsa_invocation_id',
1948                      'consecutive_sync_failures', 'last_success',
1949                      'last_attempt' ]:
1950
1951             if item in ['replica_flags']:
1952                 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
1953             elif item in ['schedule']:
1954                 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
1955
1956             setattr(self.__dict__['ndr_blob'].ctr, item, value)
1957
1958         elif item in ['dns_name1']:
1959             self.__dict__['dns_name1'] = value
1960
1961             if self.__dict__['ndr_blob'].version == 0x1:
1962                 self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
1963                     self.__dict__['dns_name1']
1964             else:
1965                 self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
1966                     self.__dict__['dns_name1']
1967
1968         elif item in ['dns_name2']:
1969             self.__dict__['dns_name2'] = value
1970
1971             if self.__dict__['ndr_blob'].version == 0x1:
1972                 raise AttributeError(item)
1973             else:
1974                 self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
1975                     self.__dict__['dns_name2']
1976
1977         elif item in ['nc_dnstr']:
1978             self.__dict__['nc_dnstr'] = value
1979
1980         elif item in ['to_be_deleted']:
1981             self.__dict__['to_be_deleted'] = value
1982
1983         elif item in ['version']:
1984             raise AttributeError, "Attempt to set readonly attribute %s" % item
1985         else:
1986             raise AttributeError, "Unknown attribute %s" % item
1987
1988         self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
1989
1990     def __getattr__(self, item):
1991         """Overload of RepsFromTo attribute retrieval.
1992
1993         Allows external code to ignore substructures within the blob
1994         """
1995         if item in [ 'schedule', 'replica_flags', 'transport_guid',
1996                      'source_dsa_obj_guid', 'source_dsa_invocation_id',
1997                      'consecutive_sync_failures', 'last_success',
1998                      'last_attempt' ]:
1999             return getattr(self.__dict__['ndr_blob'].ctr, item)
2000
2001         elif item in ['version']:
2002             return self.__dict__['ndr_blob'].version
2003
2004         elif item in ['dns_name1']:
2005             if self.__dict__['ndr_blob'].version == 0x1:
2006                 return self.__dict__['ndr_blob'].ctr.other_info.dns_name
2007             else:
2008                 return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
2009
2010         elif item in ['dns_name2']:
2011             if self.__dict__['ndr_blob'].version == 0x1:
2012                 raise AttributeError(item)
2013             else:
2014                 return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
2015
2016         elif item in ['to_be_deleted']:
2017             return self.__dict__['to_be_deleted']
2018
2019         elif item in ['nc_dnstr']:
2020             return self.__dict__['nc_dnstr']
2021
2022         elif item in ['update_flags']:
2023             return self.__dict__['update_flags']
2024
2025         raise AttributeError, "Unknwown attribute %s" % item
2026
2027     def is_modified(self):
2028         return (self.update_flags != 0x0)
2029
2030     def set_unmodified(self):
2031         self.__dict__['update_flags'] = 0x0
2032
2033
2034 class SiteLink(object):
2035     """Class defines a site link found under sites
2036     """
2037
2038     def __init__(self, dnstr):
2039         self.dnstr = dnstr
2040         self.options = 0
2041         self.system_flags = 0
2042         self.cost = 0
2043         self.schedule = None
2044         self.interval = None
2045         self.site_list = []
2046
2047     def __str__(self):
2048         '''Debug dump string output of Transport object'''
2049
2050         text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
2051         text = text + "\n\toptions=%d" % self.options
2052         text = text + "\n\tsystem_flags=%d" % self.system_flags
2053         text = text + "\n\tcost=%d" % self.cost
2054         text = text + "\n\tinterval=%s" % self.interval
2055
2056         if self.schedule is not None:
2057             text = text + "\n\tschedule.size=%s" % self.schedule.size
2058             text = text + "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
2059             text = text + "\n\tschedule.numberOfSchedules=%s" % \
2060                    self.schedule.numberOfSchedules
2061
2062             for i, header in enumerate(self.schedule.headerArray):
2063                 text = text + "\n\tschedule.headerArray[%d].type=%d" % \
2064                        (i, header.type)
2065                 text = text + "\n\tschedule.headerArray[%d].offset=%d" % \
2066                        (i, header.offset)
2067                 text = text + "\n\tschedule.dataArray[%d].slots[ " % i
2068                 for slot in self.schedule.dataArray[i].slots:
2069                     text = text + "0x%X " % slot
2070                 text = text + "]"
2071
2072         for dnstr in self.site_list:
2073             text = text + "\n\tsite_list=%s" % dnstr
2074         return text
2075
2076     def load_sitelink(self, samdb):
2077         """Given a siteLink object with an prior initialization
2078         for the object's DN, search for the DN and load attributes
2079         from the samdb.
2080         """
2081         attrs = [ "options",
2082                   "systemFlags",
2083                   "cost",
2084                   "schedule",
2085                   "replInterval",
2086                   "siteList" ]
2087         try:
2088             res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
2089                                attrs=attrs)
2090
2091         except ldb.LdbError, (enum, estr):
2092             raise Exception("Unable to find SiteLink for (%s) - (%s)" %
2093                             (self.dnstr, estr))
2094
2095         msg = res[0]
2096
2097         if "options" in msg:
2098             self.options = int(msg["options"][0])
2099
2100         if "systemFlags" in msg:
2101             self.system_flags = int(msg["systemFlags"][0])
2102
2103         if "cost" in msg:
2104             self.cost = int(msg["cost"][0])
2105
2106         if "replInterval" in msg:
2107             self.interval = int(msg["replInterval"][0])
2108
2109         if "siteList" in msg:
2110             for value in msg["siteList"]:
2111                 dsdn = dsdb_Dn(samdb, value)
2112                 dnstr = str(dsdn.dn)
2113                 if dnstr not in self.site_list:
2114                     self.site_list.append(dnstr)
2115
2116     def is_sitelink(self, site1_dnstr, site2_dnstr):
2117         """Given a siteLink object, determine if it is a link
2118         between the two input site DNs
2119         """
2120         if site1_dnstr in self.site_list and site2_dnstr in self.site_list:
2121             return True
2122         return False
2123
2124
2125 class VertexColor(object):
2126     (unknown, white, black, red) = range(0, 4)
2127
2128
2129 class Vertex(object):
2130     """Class encapsulation of a Site Vertex in the
2131     intersite topology replication algorithm
2132     """
2133     def __init__(self, site, part):
2134         self.site = site
2135         self.part = part
2136         self.color = VertexColor.unknown
2137
2138     def color_vertex(self):
2139         """Color each vertex to indicate which kind of NC
2140         replica it contains
2141         """
2142         # IF s contains one or more DCs with full replicas of the
2143         # NC cr!nCName
2144         #    SET v.Color to COLOR.RED
2145         # ELSEIF s contains one or more partial replicas of the NC
2146         #    SET v.Color to COLOR.BLACK
2147         #ELSE
2148         #    SET v.Color to COLOR.WHITE
2149
2150         # set to minimum (no replica)
2151         self.color = VertexColor.white
2152
2153         for dnstr, dsa in self.site.dsa_table.items():
2154             rep = dsa.get_current_replica(self.part.nc_dnstr)
2155             if rep is None:
2156                 continue
2157
2158             # We have a full replica which is the largest
2159             # value so exit
2160             if not rep.is_partial():
2161                 self.color = VertexColor.red
2162                 break
2163             else:
2164                 self.color = VertexColor.black
2165
2166     def is_red(self):
2167         assert(self.color != VertexColor.unknown)
2168         return (self.color == VertexColor.red)
2169
2170     def is_black(self):
2171         assert(self.color != VertexColor.unknown)
2172         return (self.color == VertexColor.black)
2173
2174     def is_white(self):
2175         assert(self.color != VertexColor.unknown)
2176         return (self.color == VertexColor.white)
2177
2178 ##################################################
2179 # Global Functions
2180 ##################################################
2181 def sort_dsa_by_guid(dsa1, dsa2):
2182     return cmp(dsa1.dsa_guid, dsa2.dsa_guid)