kcc: prevent non-determinism when running translation
[bbaumbach/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 # Copyright (C) Andrew Bartlett 2015
6 #
7 # Andrew Bartlett's alleged work performed by his underlings Douglas
8 # Bagnall and Garming Sam.
9 #
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
14 #
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
23 import ldb
24 import uuid
25
26 from samba import dsdb, unix2nttime
27 from samba.dcerpc import (
28     drsblobs,
29     drsuapi,
30     misc,
31     )
32 from samba.common import dsdb_Dn
33 from samba.ndr import (ndr_unpack, ndr_pack)
34
35
36 class KCCError(Exception):
37     pass
38
39
40 class NCType(object):
41     (unknown, schema, domain, config, application) = range(0, 5)
42
43 # map the NCType enum to strings for debugging
44 nctype_lut = {v: k for k, v in NCType.__dict__.items() if k[:2] != '__'}
45
46
47 class NamingContext(object):
48     """Base class for a naming context.
49
50     Holds the DN, GUID, SID (if available) and type of the DN.
51     Subclasses may inherit from this and specialize
52     """
53
54     def __init__(self, nc_dnstr):
55         """Instantiate a NamingContext
56
57         :param nc_dnstr: NC dn string
58         """
59         self.nc_dnstr = nc_dnstr
60         self.nc_guid = None
61         self.nc_sid = None
62         self.nc_type = NCType.unknown
63
64     def __str__(self):
65         '''Debug dump string output of class'''
66         text = "%s:" % (self.__class__.__name__,)
67         text = text + "\n\tnc_dnstr=%s" % self.nc_dnstr
68         text = text + "\n\tnc_guid=%s" % str(self.nc_guid)
69
70         if self.nc_sid is None:
71             text = text + "\n\tnc_sid=<absent>"
72         else:
73             text = text + "\n\tnc_sid=<present>"
74
75         text = text + "\n\tnc_type=%s (%s)" % (nctype_lut[self.nc_type],
76                                                self.nc_type)
77         return text
78
79     def load_nc(self, samdb):
80         attrs = ["objectGUID",
81                  "objectSid"]
82         try:
83             res = samdb.search(base=self.nc_dnstr,
84                                scope=ldb.SCOPE_BASE, attrs=attrs)
85
86         except ldb.LdbError, (enum, estr):
87             raise Exception("Unable to find naming context (%s) - (%s)" %
88                             (self.nc_dnstr, estr))
89         msg = res[0]
90         if "objectGUID" in msg:
91             self.nc_guid = misc.GUID(samdb.schema_format_value("objectGUID",
92                                      msg["objectGUID"][0]))
93         if "objectSid" in msg:
94             self.nc_sid = msg["objectSid"][0]
95
96         assert self.nc_guid is not None
97
98     def is_schema(self):
99         '''Return True if NC is schema'''
100         assert self.nc_type != NCType.unknown
101         return self.nc_type == NCType.schema
102
103     def is_domain(self):
104         '''Return True if NC is domain'''
105         assert self.nc_type != NCType.unknown
106         return self.nc_type == NCType.domain
107
108     def is_application(self):
109         '''Return True if NC is application'''
110         assert self.nc_type != NCType.unknown
111         return self.nc_type == NCType.application
112
113     def is_config(self):
114         '''Return True if NC is config'''
115         assert self.nc_type != NCType.unknown
116         return self.nc_type == NCType.config
117
118     def identify_by_basedn(self, samdb):
119         """Given an NC object, identify what type is is thru
120            the samdb basedn strings and NC sid value
121         """
122         # Invoke loader to initialize guid and more
123         # importantly sid value (sid is used to identify
124         # domain NCs)
125         if self.nc_guid is None:
126             self.load_nc(samdb)
127
128         # We check against schema and config because they
129         # will be the same for all nTDSDSAs in the forest.
130         # That leaves the domain NCs which can be identified
131         # by sid and application NCs as the last identified
132         if self.nc_dnstr == str(samdb.get_schema_basedn()):
133             self.nc_type = NCType.schema
134         elif self.nc_dnstr == str(samdb.get_config_basedn()):
135             self.nc_type = NCType.config
136         elif self.nc_sid is not None:
137             self.nc_type = NCType.domain
138         else:
139             self.nc_type = NCType.application
140
141     def identify_by_dsa_attr(self, samdb, attr):
142         """Given an NC which has been discovered thru the
143         nTDSDSA database object, determine what type of NC
144         it is (i.e. schema, config, domain, application) via
145         the use of the schema attribute under which the NC
146         was found.
147
148         :param attr: attr of nTDSDSA object where NC DN appears
149         """
150         # If the NC is listed under msDS-HasDomainNCs then
151         # this can only be a domain NC and it is our default
152         # domain for this dsa
153         if attr == "msDS-HasDomainNCs":
154             self.nc_type = NCType.domain
155
156         # If the NC is listed under hasPartialReplicaNCs
157         # this is only a domain NC
158         elif attr == "hasPartialReplicaNCs":
159             self.nc_type = NCType.domain
160
161         # NCs listed under hasMasterNCs are either
162         # default domain, schema, or config.  We
163         # utilize the identify_by_basedn() to
164         # identify those
165         elif attr == "hasMasterNCs":
166             self.identify_by_basedn(samdb)
167
168         # Still unknown (unlikely) but for completeness
169         # and for finally identifying application NCs
170         if self.nc_type == NCType.unknown:
171             self.identify_by_basedn(samdb)
172
173
174 class NCReplica(NamingContext):
175     """Naming context replica that is relative to a specific DSA.
176
177     This is a more specific form of NamingContext class (inheriting from that
178     class) and it identifies unique attributes of the DSA's replica for a NC.
179     """
180
181     def __init__(self, dsa_dnstr, dsa_guid, nc_dnstr):
182         """Instantiate a Naming Context Replica
183
184         :param dsa_guid: GUID of DSA where replica appears
185         :param nc_dnstr: NC dn string
186         """
187         self.rep_dsa_dnstr = dsa_dnstr
188         self.rep_dsa_guid = dsa_guid
189         self.rep_default = False  # replica for DSA's default domain
190         self.rep_partial = False
191         self.rep_ro = False
192         self.rep_instantiated_flags = 0
193
194         self.rep_fsmo_role_owner = None
195
196         # RepsFromTo tuples
197         self.rep_repsFrom = []
198
199         # The (is present) test is a combination of being
200         # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or
201         # hasPartialReplicaNCs) as well as its replica flags found
202         # thru the msDS-HasInstantiatedNCs.  If the NC replica meets
203         # the first enumeration test then this flag is set true
204         self.rep_present_criteria_one = False
205
206         # Call my super class we inherited from
207         NamingContext.__init__(self, nc_dnstr)
208
209     def __str__(self):
210         '''Debug dump string output of class'''
211         text = "%s:" % self.__class__.__name__
212         text = text + "\n\tdsa_dnstr=%s" % self.rep_dsa_dnstr
213         text = text + "\n\tdsa_guid=%s" % self.rep_dsa_guid
214         text = text + "\n\tdefault=%s" % self.rep_default
215         text = text + "\n\tro=%s" % self.rep_ro
216         text = text + "\n\tpartial=%s" % self.rep_partial
217         text = text + "\n\tpresent=%s" % self.is_present()
218         text = text + "\n\tfsmo_role_owner=%s" % self.rep_fsmo_role_owner
219
220         for rep in self.rep_repsFrom:
221             text = text + "\n%s" % rep
222
223         return "%s\n%s" % (NamingContext.__str__(self), text)
224
225     def set_instantiated_flags(self, flags=None):
226         '''Set or clear NC replica instantiated flags'''
227         if flags is None:
228             self.rep_instantiated_flags = 0
229         else:
230             self.rep_instantiated_flags = flags
231
232     def identify_by_dsa_attr(self, samdb, attr):
233         """Given an NC which has been discovered thru the
234         nTDSDSA database object, determine what type of NC
235         replica it is (i.e. partial, read only, default)
236
237         :param attr: attr of nTDSDSA object where NC DN appears
238         """
239         # If the NC was found under hasPartialReplicaNCs
240         # then a partial replica at this dsa
241         if attr == "hasPartialReplicaNCs":
242             self.rep_partial = True
243             self.rep_present_criteria_one = True
244
245         # If the NC is listed under msDS-HasDomainNCs then
246         # this can only be a domain NC and it is the DSA's
247         # default domain NC
248         elif attr == "msDS-HasDomainNCs":
249             self.rep_default = True
250
251         # NCs listed under hasMasterNCs are either
252         # default domain, schema, or config.  We check
253         # against schema and config because they will be
254         # the same for all nTDSDSAs in the forest.  That
255         # leaves the default domain NC remaining which
256         # may be different for each nTDSDSAs (and thus
257         # we don't compare agains this samdb's default
258         # basedn
259         elif attr == "hasMasterNCs":
260             self.rep_present_criteria_one = True
261
262             if self.nc_dnstr != str(samdb.get_schema_basedn()) and \
263                self.nc_dnstr != str(samdb.get_config_basedn()):
264                 self.rep_default = True
265
266         # RODC only
267         elif attr == "msDS-hasFullReplicaNCs":
268             self.rep_present_criteria_one = True
269             self.rep_ro = True
270
271         # Not RODC
272         elif attr == "msDS-hasMasterNCs":
273             self.rep_present_criteria_one = True
274             self.rep_ro = False
275
276         # Now use this DSA attribute to identify the naming
277         # context type by calling the super class method
278         # of the same name
279         NamingContext.identify_by_dsa_attr(self, samdb, attr)
280
281     def is_default(self):
282         """Whether this is a default domain for the dsa that this NC appears on
283         """
284         return self.rep_default
285
286     def is_ro(self):
287         '''Return True if NC replica is read only'''
288         return self.rep_ro
289
290     def is_partial(self):
291         '''Return True if NC replica is partial'''
292         return self.rep_partial
293
294     def is_present(self):
295         """Given an NC replica which has been discovered thru the
296         nTDSDSA database object and populated with replica flags
297         from the msDS-HasInstantiatedNCs; return whether the NC
298         replica is present (true) or if the IT_NC_GOING flag is
299         set then the NC replica is not present (false)
300         """
301         if self.rep_present_criteria_one and \
302            self.rep_instantiated_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0:
303             return True
304         return False
305
306     def load_repsFrom(self, samdb):
307         """Given an NC replica which has been discovered thru the nTDSDSA
308         database object, load the repsFrom attribute for the local replica.
309         held by my dsa.  The repsFrom attribute is not replicated so this
310         attribute is relative only to the local DSA that the samdb exists on
311         """
312         try:
313             res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
314                                attrs=["repsFrom"])
315
316         except ldb.LdbError, (enum, estr):
317             raise Exception("Unable to find NC for (%s) - (%s)" %
318                             (self.nc_dnstr, estr))
319
320         msg = res[0]
321
322         # Possibly no repsFrom if this is a singleton DC
323         if "repsFrom" in msg:
324             for value in msg["repsFrom"]:
325                 rep = RepsFromTo(self.nc_dnstr,
326                                  ndr_unpack(drsblobs.repsFromToBlob, value))
327                 self.rep_repsFrom.append(rep)
328
329     def commit_repsFrom(self, samdb, ro=False):
330         """Commit repsFrom to the database"""
331
332         # XXX - This is not truly correct according to the MS-TECH
333         #       docs.  To commit a repsFrom we should be using RPCs
334         #       IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
335         #       IDL_DRSReplicaDel to affect a repsFrom change.
336         #
337         #       Those RPCs are missing in samba, so I'll have to
338         #       implement them to get this to more accurately
339         #       reflect the reference docs.  As of right now this
340         #       commit to the database will work as its what the
341         #       older KCC also did
342         modify = False
343         newreps = []
344         delreps = []
345
346         for repsFrom in self.rep_repsFrom:
347
348             # Leave out any to be deleted from
349             # replacement list.  Build a list
350             # of to be deleted reps which we will
351             # remove from rep_repsFrom list below
352             if repsFrom.to_be_deleted:
353                 delreps.append(repsFrom)
354                 modify = True
355                 continue
356
357             if repsFrom.is_modified():
358                 repsFrom.set_unmodified()
359                 modify = True
360
361             # current (unmodified) elements also get
362             # appended here but no changes will occur
363             # unless something is "to be modified" or
364             # "to be deleted"
365             newreps.append(ndr_pack(repsFrom.ndr_blob))
366
367         # Now delete these from our list of rep_repsFrom
368         for repsFrom in delreps:
369             self.rep_repsFrom.remove(repsFrom)
370         delreps = []
371
372         # Nothing to do if no reps have been modified or
373         # need to be deleted or input option has informed
374         # us to be "readonly" (ro).  Leave database
375         # record "as is"
376         if not modify or ro:
377             return
378
379         m = ldb.Message()
380         m.dn = ldb.Dn(samdb, self.nc_dnstr)
381
382         m["repsFrom"] = \
383             ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsFrom")
384
385         try:
386             samdb.modify(m)
387
388         except ldb.LdbError, estr:
389             raise Exception("Could not set repsFrom for (%s) - (%s)" %
390                             (self.nc_dnstr, estr))
391
392     def load_replUpToDateVector(self, samdb):
393         """Given an NC replica which has been discovered thru the nTDSDSA
394         database object, load the replUpToDateVector attribute for the
395         local replica. held by my dsa. The replUpToDateVector
396         attribute is not replicated so this attribute is relative only
397         to the local DSA that the samdb exists on
398
399         """
400         try:
401             res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
402                                attrs=["replUpToDateVector"])
403
404         except ldb.LdbError, (enum, estr):
405             raise Exception("Unable to find NC for (%s) - (%s)" %
406                             (self.nc_dnstr, estr))
407
408         msg = res[0]
409
410         # Possibly no replUpToDateVector if this is a singleton DC
411         if "replUpToDateVector" in msg:
412             value = msg["replUpToDateVector"][0]
413             blob = ndr_unpack(drsblobs.replUpToDateVectorBlob,
414                               value)
415             if blob.version != 2:
416                 # Samba only generates version 2, and this runs locally
417                 raise AttributeError("Unexpected replUpToDateVector version %d"
418                                      % blob.version)
419
420             self.rep_replUpToDateVector_cursors = blob.ctr.cursors
421         else:
422             self.rep_replUpToDateVector_cursors = []
423
424     def dumpstr_to_be_deleted(self):
425         return '\n'.join(str(x) for x in self.rep_repsFrom if x.to_be_deleted)
426
427     def dumpstr_to_be_modified(self):
428         return '\n'.join(str(x) for x in self.rep_repsFrom if x.is_modified())
429
430     def load_fsmo_roles(self, samdb):
431         """Given an NC replica which has been discovered thru the nTDSDSA
432         database object, load the fSMORoleOwner attribute.
433         """
434         try:
435             res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
436                                attrs=["fSMORoleOwner"])
437
438         except ldb.LdbError, (enum, estr):
439             raise Exception("Unable to find NC for (%s) - (%s)" %
440                             (self.nc_dnstr, estr))
441
442         msg = res[0]
443
444         # Possibly no fSMORoleOwner
445         if "fSMORoleOwner" in msg:
446             self.rep_fsmo_role_owner = msg["fSMORoleOwner"]
447
448     def is_fsmo_role_owner(self, dsa_dnstr):
449         if self.rep_fsmo_role_owner is not None and \
450            self.rep_fsmo_role_owner == dsa_dnstr:
451             return True
452         return False
453
454
455 class DirectoryServiceAgent(object):
456
457     def __init__(self, dsa_dnstr):
458         """Initialize DSA class.
459
460         Class is subsequently fully populated by calling the load_dsa() method
461
462         :param dsa_dnstr:  DN of the nTDSDSA
463         """
464         self.dsa_dnstr = dsa_dnstr
465         self.dsa_guid = None
466         self.dsa_ivid = None
467         self.dsa_is_ro = False
468         self.dsa_is_istg = False
469         self.dsa_options = 0
470         self.dsa_behavior = 0
471         self.default_dnstr = None  # default domain dn string for dsa
472
473         # NCReplicas for this dsa that are "present"
474         # Indexed by DN string of naming context
475         self.current_rep_table = {}
476
477         # NCReplicas for this dsa that "should be present"
478         # Indexed by DN string of naming context
479         self.needed_rep_table = {}
480
481         # NTDSConnections for this dsa.  These are current
482         # valid connections that are committed or pending a commit
483         # in the database.  Indexed by DN string of connection
484         self.connect_table = {}
485
486     def __str__(self):
487         '''Debug dump string output of class'''
488
489         text = "%s:" % self.__class__.__name__
490         if self.dsa_dnstr is not None:
491             text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
492         if self.dsa_guid is not None:
493             text = text + "\n\tdsa_guid=%s" % str(self.dsa_guid)
494         if self.dsa_ivid is not None:
495             text = text + "\n\tdsa_ivid=%s" % str(self.dsa_ivid)
496
497         text = text + "\n\tro=%s" % self.is_ro()
498         text = text + "\n\tgc=%s" % self.is_gc()
499         text = text + "\n\tistg=%s" % self.is_istg()
500
501         text = text + "\ncurrent_replica_table:"
502         text = text + "\n%s" % self.dumpstr_current_replica_table()
503         text = text + "\nneeded_replica_table:"
504         text = text + "\n%s" % self.dumpstr_needed_replica_table()
505         text = text + "\nconnect_table:"
506         text = text + "\n%s" % self.dumpstr_connect_table()
507
508         return text
509
510     def get_current_replica(self, nc_dnstr):
511         return self.current_rep_table.get(nc_dnstr)
512
513     def is_istg(self):
514         '''Returns True if dsa is intersite topology generator for it's site'''
515         # The KCC on an RODC always acts as an ISTG for itself
516         return self.dsa_is_istg or self.dsa_is_ro
517
518     def is_ro(self):
519         '''Returns True if dsa a read only domain controller'''
520         return self.dsa_is_ro
521
522     def is_gc(self):
523         '''Returns True if dsa hosts a global catalog'''
524         if (self.options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
525             return True
526         return False
527
528     def is_minimum_behavior(self, version):
529         """Is dsa at minimum windows level greater than or equal to (version)
530
531         :param version: Windows version to test against
532             (e.g. DS_DOMAIN_FUNCTION_2008)
533         """
534         if self.dsa_behavior >= version:
535             return True
536         return False
537
538     def is_translate_ntdsconn_disabled(self):
539         """Whether this allows NTDSConnection translation in its options."""
540         if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0:
541             return True
542         return False
543
544     def get_rep_tables(self):
545         """Return DSA current and needed replica tables
546         """
547         return self.current_rep_table, self.needed_rep_table
548
549     def get_parent_dnstr(self):
550         """Get the parent DN string of this object."""
551         head, sep, tail = self.dsa_dnstr.partition(',')
552         return tail
553
554     def load_dsa(self, samdb):
555         """Load a DSA from the samdb.
556
557         Prior initialization has given us the DN of the DSA that we are to
558         load.  This method initializes all other attributes, including loading
559         the NC replica table for this DSA.
560         """
561         attrs = ["objectGUID",
562                  "invocationID",
563                  "options",
564                  "msDS-isRODC",
565                  "msDS-Behavior-Version"]
566         try:
567             res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
568                                attrs=attrs)
569
570         except ldb.LdbError, (enum, estr):
571             raise Exception("Unable to find nTDSDSA for (%s) - (%s)" %
572                             (self.dsa_dnstr, estr))
573
574         msg = res[0]
575         self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID",
576                                   msg["objectGUID"][0]))
577
578         # RODCs don't originate changes and thus have no invocationId,
579         # therefore we must check for existence first
580         if "invocationId" in msg:
581             self.dsa_ivid = misc.GUID(samdb.schema_format_value("objectGUID",
582                                       msg["invocationId"][0]))
583
584         if "options" in msg:
585             self.options = int(msg["options"][0])
586
587         if "msDS-isRODC" in msg and msg["msDS-isRODC"][0] == "TRUE":
588             self.dsa_is_ro = True
589         else:
590             self.dsa_is_ro = False
591
592         if "msDS-Behavior-Version" in msg:
593             self.dsa_behavior = int(msg['msDS-Behavior-Version'][0])
594
595         # Load the NC replicas that are enumerated on this dsa
596         self.load_current_replica_table(samdb)
597
598         # Load the nTDSConnection that are enumerated on this dsa
599         self.load_connection_table(samdb)
600
601     def load_current_replica_table(self, samdb):
602         """Method to load the NC replica's listed for DSA object.
603
604         This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs,
605         hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, and
606         msDS-HasInstantiatedNCs) to determine complete list of NC replicas that
607         are enumerated for the DSA.  Once a NC replica is loaded it is
608         identified (schema, config, etc) and the other replica attributes
609         (partial, ro, etc) are determined.
610
611         :param samdb: database to query for DSA replica list
612         """
613         ncattrs = [
614             # not RODC - default, config, schema (old style)
615             "hasMasterNCs",
616             # not RODC - default, config, schema, app NCs
617             "msDS-hasMasterNCs",
618             # domain NC partial replicas
619             "hasPartialReplicaNCs",
620             # default domain NC
621             "msDS-HasDomainNCs",
622             # RODC only - default, config, schema, app NCs
623             "msDS-hasFullReplicaNCs",
624             # Identifies if replica is coming, going, or stable
625             "msDS-HasInstantiatedNCs"
626         ]
627         try:
628             res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
629                                attrs=ncattrs)
630
631         except ldb.LdbError, (enum, estr):
632             raise Exception("Unable to find nTDSDSA NCs for (%s) - (%s)" %
633                             (self.dsa_dnstr, estr))
634
635         # The table of NCs for the dsa we are searching
636         tmp_table = {}
637
638         # We should get one response to our query here for
639         # the ntds that we requested
640         if len(res[0]) > 0:
641
642             # Our response will contain a number of elements including
643             # the dn of the dsa as well as elements for each
644             # attribute (e.g. hasMasterNCs).  Each of these elements
645             # is a dictonary list which we retrieve the keys for and
646             # then iterate over them
647             for k in res[0].keys():
648                 if k == "dn":
649                     continue
650
651                 # For each attribute type there will be one or more DNs
652                 # listed.  For instance DCs normally have 3 hasMasterNCs
653                 # listed.
654                 for value in res[0][k]:
655                     # Turn dn into a dsdb_Dn so we can use
656                     # its methods to parse a binary DN
657                     dsdn = dsdb_Dn(samdb, value)
658                     flags = dsdn.get_binary_integer()
659                     dnstr = str(dsdn.dn)
660
661                     if not dnstr in tmp_table:
662                         rep = NCReplica(self.dsa_dnstr, self.dsa_guid, dnstr)
663                         tmp_table[dnstr] = rep
664                     else:
665                         rep = tmp_table[dnstr]
666
667                     if k == "msDS-HasInstantiatedNCs":
668                         rep.set_instantiated_flags(flags)
669                         continue
670
671                     rep.identify_by_dsa_attr(samdb, k)
672
673                     # if we've identified the default domain NC
674                     # then save its DN string
675                     if rep.is_default():
676                         self.default_dnstr = dnstr
677         else:
678             raise Exception("No nTDSDSA NCs for (%s)" % self.dsa_dnstr)
679
680         # Assign our newly built NC replica table to this dsa
681         self.current_rep_table = tmp_table
682
683     def add_needed_replica(self, rep):
684         """Method to add a NC replica that "should be present" to the
685         needed_rep_table.
686         """
687         self.needed_rep_table[rep.nc_dnstr] = rep
688
689     def load_connection_table(self, samdb):
690         """Method to load the nTDSConnections listed for DSA object.
691
692         :param samdb: database to query for DSA connection list
693         """
694         try:
695             res = samdb.search(base=self.dsa_dnstr,
696                                scope=ldb.SCOPE_SUBTREE,
697                                expression="(objectClass=nTDSConnection)")
698
699         except ldb.LdbError, (enum, estr):
700             raise Exception("Unable to find nTDSConnection for (%s) - (%s)" %
701                             (self.dsa_dnstr, estr))
702
703         for msg in res:
704             dnstr = str(msg.dn)
705
706             # already loaded
707             if dnstr in self.connect_table:
708                 continue
709
710             connect = NTDSConnection(dnstr)
711
712             connect.load_connection(samdb)
713             self.connect_table[dnstr] = connect
714
715     def commit_connections(self, samdb, ro=False):
716         """Method to commit any uncommitted nTDSConnections
717         modifications that are in our table.  These would be
718         identified connections that are marked to be added or
719         deleted
720
721         :param samdb: database to commit DSA connection list to
722         :param ro: if (true) then peform internal operations but
723             do not write to the database (readonly)
724         """
725         delconn = []
726
727         for dnstr, connect in self.connect_table.items():
728             if connect.to_be_added:
729                 connect.commit_added(samdb, ro)
730
731             if connect.to_be_modified:
732                 connect.commit_modified(samdb, ro)
733
734             if connect.to_be_deleted:
735                 connect.commit_deleted(samdb, ro)
736                 delconn.append(dnstr)
737
738         # Now delete the connection from the table
739         for dnstr in delconn:
740             del self.connect_table[dnstr]
741
742     def add_connection(self, dnstr, connect):
743         assert dnstr not in self.connect_table
744         self.connect_table[dnstr] = connect
745
746     def get_connection_by_from_dnstr(self, from_dnstr):
747         """Scan DSA nTDSConnection table and return connection
748         with a "fromServer" dn string equivalent to method
749         input parameter.
750
751         :param from_dnstr: search for this from server entry
752         """
753         answer = []
754         for connect in self.connect_table.values():
755             if connect.get_from_dnstr() == from_dnstr:
756                 answer.append(connect)
757
758         return answer
759
760     def dumpstr_current_replica_table(self):
761         '''Debug dump string output of current replica table'''
762         return '\n'.join(str(x) for x in self.current_rep_table)
763
764     def dumpstr_needed_replica_table(self):
765         '''Debug dump string output of needed replica table'''
766         return '\n'.join(str(x) for x in self.needed_rep_table)
767
768     def dumpstr_connect_table(self):
769         '''Debug dump string output of connect table'''
770         return '\n'.join(str(x) for x in self.connect_table)
771
772     def new_connection(self, options, flags, transport, from_dnstr, sched):
773         """Set up a new connection for the DSA based on input
774         parameters.  Connection will be added to the DSA
775         connect_table and will be marked as "to be added" pending
776         a call to commit_connections()
777         """
778         dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
779
780         connect = NTDSConnection(dnstr)
781         connect.to_be_added = True
782         connect.enabled = True
783         connect.from_dnstr = from_dnstr
784         connect.options = options
785         connect.flags = flags
786
787         if transport is not None:
788             connect.transport_dnstr = transport.dnstr
789             connect.transport_guid = transport.guid
790
791         if sched is not None:
792             connect.schedule = sched
793         else:
794             # Create schedule.  Attribute valuse set according to MS-TECH
795             # intrasite connection creation document
796             connect.schedule = new_connection_schedule()
797
798         self.add_connection(dnstr, connect)
799         return connect
800
801
802 class NTDSConnection(object):
803     """Class defines a nTDSConnection found under a DSA
804     """
805     def __init__(self, dnstr):
806         self.dnstr = dnstr
807         self.guid = None
808         self.enabled = False
809         self.whenCreated = 0
810         self.to_be_added = False  # new connection needs to be added
811         self.to_be_deleted = False  # old connection needs to be deleted
812         self.to_be_modified = False
813         self.options = 0
814         self.system_flags = 0
815         self.transport_dnstr = None
816         self.transport_guid = None
817         self.from_dnstr = None
818         self.schedule = None
819
820     def __str__(self):
821         '''Debug dump string output of NTDSConnection object'''
822
823         text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
824         text = text + "\n\tenabled=%s" % self.enabled
825         text = text + "\n\tto_be_added=%s" % self.to_be_added
826         text = text + "\n\tto_be_deleted=%s" % self.to_be_deleted
827         text = text + "\n\tto_be_modified=%s" % self.to_be_modified
828         text = text + "\n\toptions=0x%08X" % self.options
829         text = text + "\n\tsystem_flags=0x%08X" % self.system_flags
830         text = text + "\n\twhenCreated=%d" % self.whenCreated
831         text = text + "\n\ttransport_dn=%s" % self.transport_dnstr
832
833         if self.guid is not None:
834             text = text + "\n\tguid=%s" % str(self.guid)
835
836         if self.transport_guid is not None:
837             text = text + "\n\ttransport_guid=%s" % str(self.transport_guid)
838
839         text = text + "\n\tfrom_dn=%s" % self.from_dnstr
840
841         if self.schedule is not None:
842             text += "\n\tschedule.size=%s" % self.schedule.size
843             text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
844             text += ("\n\tschedule.numberOfSchedules=%s" %
845                      self.schedule.numberOfSchedules)
846
847             for i, header in enumerate(self.schedule.headerArray):
848                 text += ("\n\tschedule.headerArray[%d].type=%d" %
849                          (i, header.type))
850                 text += ("\n\tschedule.headerArray[%d].offset=%d" %
851                          (i, header.offset))
852                 text += "\n\tschedule.dataArray[%d].slots[ " % i
853                 for slot in self.schedule.dataArray[i].slots:
854                     text = text + "0x%X " % slot
855                 text = text + "]"
856
857         return text
858
859     def load_connection(self, samdb):
860         """Given a NTDSConnection object with an prior initialization
861         for the object's DN, search for the DN and load attributes
862         from the samdb.
863         """
864         attrs = ["options",
865                  "enabledConnection",
866                  "schedule",
867                  "whenCreated",
868                  "objectGUID",
869                  "transportType",
870                  "fromServer",
871                  "systemFlags"]
872         try:
873             res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
874                                attrs=attrs)
875
876         except ldb.LdbError, (enum, estr):
877             raise Exception("Unable to find nTDSConnection for (%s) - (%s)" %
878                             (self.dnstr, estr))
879
880         msg = res[0]
881
882         if "options" in msg:
883             self.options = int(msg["options"][0])
884
885         if "enabledConnection" in msg:
886             if msg["enabledConnection"][0].upper().lstrip().rstrip() == "TRUE":
887                 self.enabled = True
888
889         if "systemFlags" in msg:
890             self.system_flags = int(msg["systemFlags"][0])
891
892         if "objectGUID" in msg:
893             self.guid = \
894                 misc.GUID(samdb.schema_format_value("objectGUID",
895                                                     msg["objectGUID"][0]))
896
897         if "transportType" in msg:
898             dsdn = dsdb_Dn(samdb, msg["transportType"][0])
899             self.load_connection_transport(samdb, str(dsdn.dn))
900
901         if "schedule" in msg:
902             self.schedule = ndr_unpack(drsblobs.schedule, msg["schedule"][0])
903
904         if "whenCreated" in msg:
905             self.whenCreated = ldb.string_to_time(msg["whenCreated"][0])
906
907         if "fromServer" in msg:
908             dsdn = dsdb_Dn(samdb, msg["fromServer"][0])
909             self.from_dnstr = str(dsdn.dn)
910             assert self.from_dnstr is not None
911
912     def load_connection_transport(self, samdb, tdnstr):
913         """Given a NTDSConnection object which enumerates a transport
914         DN, load the transport information for the connection object
915
916         :param tdnstr: transport DN to load
917         """
918         attrs = ["objectGUID"]
919         try:
920             res = samdb.search(base=tdnstr,
921                                scope=ldb.SCOPE_BASE, attrs=attrs)
922
923         except ldb.LdbError, (enum, estr):
924             raise Exception("Unable to find transport (%s) - (%s)" %
925                             (tdnstr, estr))
926
927         if "objectGUID" in res[0]:
928             msg = res[0]
929             self.transport_dnstr = tdnstr
930             self.transport_guid = \
931                 misc.GUID(samdb.schema_format_value("objectGUID",
932                                                     msg["objectGUID"][0]))
933         assert self.transport_dnstr is not None
934         assert self.transport_guid is not None
935
936     def commit_deleted(self, samdb, ro=False):
937         """Local helper routine for commit_connections() which
938         handles committed connections that are to be deleted from
939         the database database
940         """
941         assert self.to_be_deleted
942         self.to_be_deleted = False
943
944         # No database modification requested
945         if ro:
946             return
947
948         try:
949             samdb.delete(self.dnstr)
950         except ldb.LdbError, (enum, estr):
951             raise Exception("Could not delete nTDSConnection for (%s) - (%s)" %
952                             (self.dnstr, estr))
953
954     def commit_added(self, samdb, ro=False):
955         """Local helper routine for commit_connections() which
956         handles committed connections that are to be added to the
957         database
958         """
959         assert self.to_be_added
960         self.to_be_added = False
961
962         # No database modification requested
963         if ro:
964             return
965
966         # First verify we don't have this entry to ensure nothing
967         # is programatically amiss
968         found = False
969         try:
970             msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
971             if len(msg) != 0:
972                 found = True
973
974         except ldb.LdbError, (enum, estr):
975             if enum != ldb.ERR_NO_SUCH_OBJECT:
976                 raise Exception("Unable to search for (%s) - (%s)" %
977                                 (self.dnstr, estr))
978         if found:
979             raise Exception("nTDSConnection for (%s) already exists!" %
980                             self.dnstr)
981
982         if self.enabled:
983             enablestr = "TRUE"
984         else:
985             enablestr = "FALSE"
986
987         # Prepare a message for adding to the samdb
988         m = ldb.Message()
989         m.dn = ldb.Dn(samdb, self.dnstr)
990
991         m["objectClass"] = \
992             ldb.MessageElement("nTDSConnection", ldb.FLAG_MOD_ADD,
993                                "objectClass")
994         m["showInAdvancedViewOnly"] = \
995             ldb.MessageElement("TRUE", ldb.FLAG_MOD_ADD,
996                                "showInAdvancedViewOnly")
997         m["enabledConnection"] = \
998             ldb.MessageElement(enablestr, ldb.FLAG_MOD_ADD,
999                                "enabledConnection")
1000         m["fromServer"] = \
1001             ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_ADD, "fromServer")
1002         m["options"] = \
1003             ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options")
1004         m["systemFlags"] = \
1005             ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_ADD,
1006                                "systemFlags")
1007
1008         if self.transport_dnstr is not None:
1009             m["transportType"] = \
1010                 ldb.MessageElement(str(self.transport_dnstr), ldb.FLAG_MOD_ADD,
1011                                    "transportType")
1012
1013         if self.schedule is not None:
1014             m["schedule"] = \
1015                 ldb.MessageElement(ndr_pack(self.schedule),
1016                                    ldb.FLAG_MOD_ADD, "schedule")
1017         try:
1018             samdb.add(m)
1019         except ldb.LdbError, (enum, estr):
1020             raise Exception("Could not add nTDSConnection for (%s) - (%s)" %
1021                             (self.dnstr, estr))
1022
1023     def commit_modified(self, samdb, ro=False):
1024         """Local helper routine for commit_connections() which
1025         handles committed connections that are to be modified to the
1026         database
1027         """
1028         assert self.to_be_modified
1029         self.to_be_modified = False
1030
1031         # No database modification requested
1032         if ro:
1033             return
1034
1035         # First verify we have this entry to ensure nothing
1036         # is programatically amiss
1037         try:
1038             #XXX msg is never used
1039             msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1040             found = True
1041
1042         except ldb.LdbError, (enum, estr):
1043             if enum == ldb.ERR_NO_SUCH_OBJECT:
1044                 found = False
1045             else:
1046                 raise Exception("Unable to search for (%s) - (%s)" %
1047                                 (self.dnstr, estr))
1048         if not found:
1049             raise Exception("nTDSConnection for (%s) doesn't exist!" %
1050                             self.dnstr)
1051
1052         if self.enabled:
1053             enablestr = "TRUE"
1054         else:
1055             enablestr = "FALSE"
1056
1057         # Prepare a message for modifying the samdb
1058         m = ldb.Message()
1059         m.dn = ldb.Dn(samdb, self.dnstr)
1060
1061         m["enabledConnection"] = \
1062             ldb.MessageElement(enablestr, ldb.FLAG_MOD_REPLACE,
1063                                "enabledConnection")
1064         m["fromServer"] = \
1065             ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_REPLACE,
1066                                "fromServer")
1067         m["options"] = \
1068             ldb.MessageElement(str(self.options), ldb.FLAG_MOD_REPLACE,
1069                                "options")
1070         m["systemFlags"] = \
1071             ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_REPLACE,
1072                                "systemFlags")
1073
1074         if self.transport_dnstr is not None:
1075             m["transportType"] = \
1076                 ldb.MessageElement(str(self.transport_dnstr),
1077                                    ldb.FLAG_MOD_REPLACE, "transportType")
1078         else:
1079             m["transportType"] = \
1080                 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "transportType")
1081
1082         if self.schedule is not None:
1083             m["schedule"] = \
1084                 ldb.MessageElement(ndr_pack(self.schedule),
1085                                    ldb.FLAG_MOD_REPLACE, "schedule")
1086         else:
1087             m["schedule"] = \
1088                 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "schedule")
1089         try:
1090             samdb.modify(m)
1091         except ldb.LdbError, (enum, estr):
1092             raise Exception("Could not modify nTDSConnection for (%s) - (%s)" %
1093                             (self.dnstr, estr))
1094
1095     def set_modified(self, truefalse):
1096         self.to_be_modified = truefalse
1097
1098     def set_added(self, truefalse):
1099         self.to_be_added = truefalse
1100
1101     def set_deleted(self, truefalse):
1102         self.to_be_deleted = truefalse
1103
1104     def is_schedule_minimum_once_per_week(self):
1105         """Returns True if our schedule includes at least one
1106         replication interval within the week.  False otherwise
1107         """
1108         # replinfo schedule is None means "always", while
1109         # NTDSConnection schedule is None means "never".
1110         if self.schedule is None or self.schedule.dataArray[0] is None:
1111             return False
1112
1113         for slot in self.schedule.dataArray[0].slots:
1114             if (slot & 0x0F) != 0x0:
1115                 return True
1116         return False
1117
1118     def is_equivalent_schedule(self, sched):
1119         """Returns True if our schedule is equivalent to the input
1120         comparison schedule.
1121
1122         :param shed: schedule to compare to
1123         """
1124         if self.schedule is not None:
1125             if sched is None:
1126                 return False
1127         elif sched is None:
1128             return True
1129
1130         if ((self.schedule.size != sched.size or
1131              self.schedule.bandwidth != sched.bandwidth or
1132              self.schedule.numberOfSchedules != sched.numberOfSchedules)):
1133             return False
1134
1135         for i, header in enumerate(self.schedule.headerArray):
1136
1137             if self.schedule.headerArray[i].type != sched.headerArray[i].type:
1138                 return False
1139
1140             if self.schedule.headerArray[i].offset != \
1141                sched.headerArray[i].offset:
1142                 return False
1143
1144             for a, b in zip(self.schedule.dataArray[i].slots,
1145                             sched.dataArray[i].slots):
1146                 if a != b:
1147                     return False
1148         return True
1149
1150     def is_rodc_topology(self):
1151         """Returns True if NTDS Connection specifies RODC
1152         topology only
1153         """
1154         if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
1155             return False
1156         return True
1157
1158     def is_generated(self):
1159         """Returns True if NTDS Connection was generated by the
1160         KCC topology algorithm as opposed to set by the administrator
1161         """
1162         if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0:
1163             return False
1164         return True
1165
1166     def is_override_notify_default(self):
1167         """Returns True if NTDS Connection should override notify default
1168         """
1169         if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0:
1170             return False
1171         return True
1172
1173     def is_use_notify(self):
1174         """Returns True if NTDS Connection should use notify
1175         """
1176         if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0:
1177             return False
1178         return True
1179
1180     def is_twoway_sync(self):
1181         """Returns True if NTDS Connection should use twoway sync
1182         """
1183         if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0:
1184             return False
1185         return True
1186
1187     def is_intersite_compression_disabled(self):
1188         """Returns True if NTDS Connection intersite compression
1189         is disabled
1190         """
1191         if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0:
1192             return False
1193         return True
1194
1195     def is_user_owned_schedule(self):
1196         """Returns True if NTDS Connection has a user owned schedule
1197         """
1198         if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0:
1199             return False
1200         return True
1201
1202     def is_enabled(self):
1203         """Returns True if NTDS Connection is enabled
1204         """
1205         return self.enabled
1206
1207     def get_from_dnstr(self):
1208         '''Return fromServer dn string attribute'''
1209         return self.from_dnstr
1210
1211
1212 class Partition(NamingContext):
1213     """A naming context discovered thru Partitions DN of the config schema.
1214
1215     This is a more specific form of NamingContext class (inheriting from that
1216     class) and it identifies unique attributes enumerated in the Partitions
1217     such as which nTDSDSAs are cross referenced for replicas
1218     """
1219     def __init__(self, partstr):
1220         self.partstr = partstr
1221         self.enabled = True
1222         self.system_flags = 0
1223         self.rw_location_list = []
1224         self.ro_location_list = []
1225
1226         # We don't have enough info to properly
1227         # fill in the naming context yet.  We'll get that
1228         # fully set up with load_partition().
1229         NamingContext.__init__(self, None)
1230
1231     def load_partition(self, samdb):
1232         """Given a Partition class object that has been initialized with its
1233         partition dn string, load the partition from the sam database, identify
1234         the type of the partition (schema, domain, etc) and record the list of
1235         nTDSDSAs that appear in the cross reference attributes
1236         msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations.
1237
1238         :param samdb: sam database to load partition from
1239         """
1240         attrs = ["nCName",
1241                  "Enabled",
1242                  "systemFlags",
1243                  "msDS-NC-Replica-Locations",
1244                  "msDS-NC-RO-Replica-Locations"]
1245         try:
1246             res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE,
1247                                attrs=attrs)
1248
1249         except ldb.LdbError, (enum, estr):
1250             raise Exception("Unable to find partition for (%s) - (%s)" % (
1251                             self.partstr, estr))
1252
1253         msg = res[0]
1254         for k in msg.keys():
1255             if k == "dn":
1256                 continue
1257
1258             if k == "Enabled":
1259                 if msg[k][0].upper().lstrip().rstrip() == "TRUE":
1260                     self.enabled = True
1261                 else:
1262                     self.enabled = False
1263                 continue
1264
1265             if k == "systemFlags":
1266                 self.system_flags = int(msg[k][0])
1267                 continue
1268
1269             for value in msg[k]:
1270                 dsdn = dsdb_Dn(samdb, value)
1271                 dnstr = str(dsdn.dn)
1272
1273                 if k == "nCName":
1274                     self.nc_dnstr = dnstr
1275                     continue
1276
1277                 if k == "msDS-NC-Replica-Locations":
1278                     self.rw_location_list.append(dnstr)
1279                     continue
1280
1281                 if k == "msDS-NC-RO-Replica-Locations":
1282                     self.ro_location_list.append(dnstr)
1283                     continue
1284
1285         # Now identify what type of NC this partition
1286         # enumerated
1287         self.identify_by_basedn(samdb)
1288
1289     def is_enabled(self):
1290         """Returns True if partition is enabled
1291         """
1292         return self.is_enabled
1293
1294     def is_foreign(self):
1295         """Returns True if this is not an Active Directory NC in our
1296         forest but is instead something else (e.g. a foreign NC)
1297         """
1298         if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0:
1299             return True
1300         else:
1301             return False
1302
1303     def should_be_present(self, target_dsa):
1304         """Tests whether this partition should have an NC replica
1305         on the target dsa.  This method returns a tuple of
1306         needed=True/False, ro=True/False, partial=True/False
1307
1308         :param target_dsa: should NC be present on target dsa
1309         """
1310         ro = False
1311         partial = False
1312
1313         # If this is the config, schema, or default
1314         # domain NC for the target dsa then it should
1315         # be present
1316         needed = (self.nc_type == NCType.config or
1317                   self.nc_type == NCType.schema or
1318                   (self.nc_type == NCType.domain and
1319                    self.nc_dnstr == target_dsa.default_dnstr))
1320
1321         # A writable replica of an application NC should be present
1322         # if there a cross reference to the target DSA exists.  Depending
1323         # on whether the DSA is ro we examine which type of cross reference
1324         # to look for (msDS-NC-Replica-Locations or
1325         # msDS-NC-RO-Replica-Locations
1326         if self.nc_type == NCType.application:
1327             if target_dsa.is_ro():
1328                 if target_dsa.dsa_dnstr in self.ro_location_list:
1329                     needed = True
1330             else:
1331                 if target_dsa.dsa_dnstr in self.rw_location_list:
1332                     needed = True
1333
1334         # If the target dsa is a gc then a partial replica of a
1335         # domain NC (other than the DSAs default domain) should exist
1336         # if there is also a cross reference for the DSA
1337         if (target_dsa.is_gc() and
1338             self.nc_type == NCType.domain and
1339             self.nc_dnstr != target_dsa.default_dnstr and
1340             (target_dsa.dsa_dnstr in self.ro_location_list or
1341              target_dsa.dsa_dnstr in self.rw_location_list)):
1342             needed = True
1343             partial = True
1344
1345         # partial NCs are always readonly
1346         if needed and (target_dsa.is_ro() or partial):
1347             ro = True
1348
1349         return needed, ro, partial
1350
1351     def __str__(self):
1352         '''Debug dump string output of class'''
1353         text = "%s" % NamingContext.__str__(self)
1354         text = text + "\n\tpartdn=%s" % self.partstr
1355         for k in self.rw_location_list:
1356             text = text + "\n\tmsDS-NC-Replica-Locations=%s" % k
1357         for k in self.ro_location_list:
1358             text = text + "\n\tmsDS-NC-RO-Replica-Locations=%s" % k
1359         return text
1360
1361
1362 class Site(object):
1363     """An individual site object discovered thru the configuration
1364     naming context.  Contains all DSAs that exist within the site
1365     """
1366     def __init__(self, site_dnstr, unix_now):
1367         self.site_dnstr = site_dnstr
1368         self.site_guid = None
1369         self.site_options = 0
1370         self.site_topo_generator = None
1371         self.site_topo_failover = 0  # appears to be in minutes
1372         self.dsa_table = {}
1373         self.rw_dsa_table = {}
1374         self.unix_now = unix_now
1375
1376     def load_site(self, samdb):
1377         """Loads the NTDS Site Settions options attribute for the site
1378         as well as querying and loading all DSAs that appear within
1379         the site.
1380         """
1381         ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1382         attrs = ["options",
1383                  "interSiteTopologyFailover",
1384                  "interSiteTopologyGenerator"]
1385         try:
1386             res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE,
1387                                attrs=attrs)
1388             self_res = samdb.search(base=self.site_dnstr, scope=ldb.SCOPE_BASE,
1389                                     attrs=['objectGUID'])
1390         except ldb.LdbError, (enum, estr):
1391             raise Exception("Unable to find site settings for (%s) - (%s)" %
1392                             (ssdn, estr))
1393
1394         msg = res[0]
1395         if "options" in msg:
1396             self.site_options = int(msg["options"][0])
1397
1398         if "interSiteTopologyGenerator" in msg:
1399             self.site_topo_generator = \
1400                 str(msg["interSiteTopologyGenerator"][0])
1401
1402         if "interSiteTopologyFailover" in msg:
1403             self.site_topo_failover = int(msg["interSiteTopologyFailover"][0])
1404
1405         msg = self_res[0]
1406         if "objectGUID" in msg:
1407             self.site_guid = misc.GUID(samdb.schema_format_value("objectGUID",
1408                                        msg["objectGUID"][0]))
1409
1410         self.load_all_dsa(samdb)
1411
1412     def load_all_dsa(self, samdb):
1413         """Discover all nTDSDSA thru the sites entry and
1414         instantiate and load the DSAs.  Each dsa is inserted
1415         into the dsa_table by dn string.
1416         """
1417         try:
1418             res = samdb.search(self.site_dnstr,
1419                                scope=ldb.SCOPE_SUBTREE,
1420                                expression="(objectClass=nTDSDSA)")
1421         except ldb.LdbError, (enum, estr):
1422             raise Exception("Unable to find nTDSDSAs - (%s)" % estr)
1423
1424         for msg in res:
1425             dnstr = str(msg.dn)
1426
1427             # already loaded
1428             if dnstr in self.dsa_table:
1429                 continue
1430
1431             dsa = DirectoryServiceAgent(dnstr)
1432
1433             dsa.load_dsa(samdb)
1434
1435             # Assign this dsa to my dsa table
1436             # and index by dsa dn
1437             self.dsa_table[dnstr] = dsa
1438             if not dsa.is_ro():
1439                 self.rw_dsa_table[dnstr] = dsa
1440
1441     def get_dsa_by_guidstr(self, guidstr):  # XXX unused
1442         for dsa in self.dsa_table.values():
1443             if str(dsa.dsa_guid) == guidstr:
1444                 return dsa
1445         return None
1446
1447     def get_dsa(self, dnstr):
1448         """Return a previously loaded DSA object by consulting
1449         the sites dsa_table for the provided DSA dn string
1450
1451         :return: None if DSA doesn't exist
1452         """
1453         return self.dsa_table.get(dnstr)
1454
1455     def select_istg(self, samdb, mydsa, ro):
1456         """Determine if my DC should be an intersite topology
1457         generator.  If my DC is the istg and is both a writeable
1458         DC and the database is opened in write mode then we perform
1459         an originating update to set the interSiteTopologyGenerator
1460         attribute in the NTDS Site Settings object.  An RODC always
1461         acts as an ISTG for itself.
1462         """
1463         # The KCC on an RODC always acts as an ISTG for itself
1464         if mydsa.dsa_is_ro:
1465             mydsa.dsa_is_istg = True
1466             self.site_topo_generator = mydsa.dsa_dnstr
1467             return True
1468
1469         c_rep = get_dsa_config_rep(mydsa)
1470
1471         # Load repsFrom and replUpToDateVector if not already loaded
1472         # so we can get the current state of the config replica and
1473         # whether we are getting updates from the istg
1474         c_rep.load_repsFrom(samdb)
1475
1476         c_rep.load_replUpToDateVector(samdb)
1477
1478         # From MS-ADTS 6.2.2.3.1 ISTG selection:
1479         #     First, the KCC on a writable DC determines whether it acts
1480         #     as an ISTG for its site
1481         #
1482         #     Let s be the object such that s!lDAPDisplayName = nTDSDSA
1483         #     and classSchema in s!objectClass.
1484         #
1485         #     Let D be the sequence of objects o in the site of the local
1486         #     DC such that o!objectCategory = s. D is sorted in ascending
1487         #     order by objectGUID.
1488         #
1489         # Which is a fancy way of saying "sort all the nTDSDSA objects
1490         # in the site by guid in ascending order".   Place sorted list
1491         # in D_sort[]
1492         D_sort = sorted(self.rw_dsa_table.values(), cmp=sort_dsa_by_guid)
1493
1494         # double word number of 100 nanosecond intervals since 1600s
1495         ntnow = unix2nttime(self.unix_now)
1496
1497         # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1498         # if o!interSiteTopologyFailover is 0 or has no value.
1499         #
1500         # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1501         #       so it appears we have to turn f into the same interval
1502         #
1503         #       interSiteTopologyFailover (if set) appears to be in minutes
1504         #       so we'll need to convert to senconds and then 100 nanosecond
1505         #       intervals
1506         #       XXX [MS-ADTS] 6.2.2.3.1 says it is seconds, not minutes.
1507         #
1508         #       10,000,000 is number of 100 nanosecond intervals in a second
1509         if self.site_topo_failover == 0:
1510             f = 2 * 60 * 60 * 10000000
1511         else:
1512             f = self.site_topo_failover * 60 * 10000000
1513
1514         # Let o be the site settings object for the site of the local
1515         # DC, or NULL if no such o exists.
1516         d_dsa = self.dsa_table.get(self.site_topo_generator)
1517
1518         # From MS-ADTS 6.2.2.3.1 ISTG selection:
1519         #     If o != NULL and o!interSiteTopologyGenerator is not the
1520         #     nTDSDSA object for the local DC and
1521         #     o!interSiteTopologyGenerator is an element dj of sequence D:
1522         #
1523         if d_dsa is not None and d_dsa is not mydsa:
1524             # From MS-ADTS 6.2.2.3.1 ISTG Selection:
1525             #     Let c be the cursor in the replUpToDateVector variable
1526             #     associated with the NC replica of the config NC such
1527             #     that c.uuidDsa = dj!invocationId. If no such c exists
1528             #     (No evidence of replication from current ITSG):
1529             #         Let i = j.
1530             #         Let t = 0.
1531             #
1532             #     Else if the current time < c.timeLastSyncSuccess - f
1533             #     (Evidence of time sync problem on current ISTG):
1534             #         Let i = 0.
1535             #         Let t = 0.
1536             #
1537             #     Else (Evidence of replication from current ITSG):
1538             #         Let i = j.
1539             #         Let t = c.timeLastSyncSuccess.
1540             #
1541             # last_success appears to be a double word containing
1542             #     number of 100 nanosecond intervals since the 1600s
1543             j_idx = D_sort.index(d_dsa)
1544
1545             found = False
1546             for cursor in c_rep.rep_replUpToDateVector_cursors:
1547                 if d_dsa.dsa_ivid == cursor.source_dsa_invocation_id:
1548                     found = True
1549                     break
1550
1551             if not found:
1552                 i_idx = j_idx
1553                 t_time = 0
1554
1555             #XXX doc says current time < c.timeLastSyncSuccess - f
1556             # which is true only if f is negative or clocks are wrong.
1557             # f is not negative in the default case (2 hours).
1558             elif ntnow - cursor.last_sync_success > f:
1559                 i_idx = 0
1560                 t_time = 0
1561             else:
1562                 i_idx = j_idx
1563                 t_time = cursor.last_sync_success
1564
1565         # Otherwise (Nominate local DC as ISTG):
1566         #     Let i be the integer such that di is the nTDSDSA
1567         #         object for the local DC.
1568         #     Let t = the current time.
1569         else:
1570             i_idx = D_sort.index(mydsa)
1571             t_time = ntnow
1572
1573         # Compute a function that maintains the current ISTG if
1574         # it is alive, cycles through other candidates if not.
1575         #
1576         # Let k be the integer (i + ((current time - t) /
1577         #     o!interSiteTopologyFailover)) MOD |D|.
1578         #
1579         # Note: We don't want to divide by zero here so they must
1580         #       have meant "f" instead of "o!interSiteTopologyFailover"
1581         k_idx = (i_idx + ((ntnow - t_time) / f)) % len(D_sort)
1582
1583         # The local writable DC acts as an ISTG for its site if and
1584         # only if dk is the nTDSDSA object for the local DC. If the
1585         # local DC does not act as an ISTG, the KCC skips the
1586         # remainder of this task.
1587         d_dsa = D_sort[k_idx]
1588         d_dsa.dsa_is_istg = True
1589
1590         # Update if we are the ISTG, otherwise return
1591         if d_dsa is not mydsa:
1592             return False
1593
1594         # Nothing to do
1595         if self.site_topo_generator == mydsa.dsa_dnstr:
1596             return True
1597
1598         self.site_topo_generator = mydsa.dsa_dnstr
1599
1600         # If readonly database then do not perform a
1601         # persistent update
1602         if ro:
1603             return True
1604
1605         # Perform update to the samdb
1606         ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1607
1608         m = ldb.Message()
1609         m.dn = ldb.Dn(samdb, ssdn)
1610
1611         m["interSiteTopologyGenerator"] = \
1612             ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
1613                                "interSiteTopologyGenerator")
1614         try:
1615             samdb.modify(m)
1616
1617         except ldb.LdbError, estr:
1618             raise Exception(
1619                 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1620                 (ssdn, estr))
1621         return True
1622
1623     def is_intrasite_topology_disabled(self):
1624         '''Returns True if intra-site topology is disabled for site'''
1625         return (self.site_options &
1626                 dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0
1627
1628     def is_intersite_topology_disabled(self):
1629         '''Returns True if inter-site topology is disabled for site'''
1630         return ((self.site_options &
1631                  dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED)
1632                 != 0)
1633
1634     def is_random_bridgehead_disabled(self):
1635         '''Returns True if selection of random bridgehead is disabled'''
1636         return (self.site_options &
1637                 dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0
1638
1639     def is_detect_stale_disabled(self):
1640         '''Returns True if detect stale is disabled for site'''
1641         return (self.site_options &
1642                 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0
1643
1644     def is_cleanup_ntdsconn_disabled(self):
1645         '''Returns True if NTDS Connection cleanup is disabled for site'''
1646         return (self.site_options &
1647                 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0
1648
1649     def same_site(self, dsa):
1650         '''Return True if dsa is in this site'''
1651         if self.get_dsa(dsa.dsa_dnstr):
1652             return True
1653         return False
1654
1655     def __str__(self):
1656         '''Debug dump string output of class'''
1657         text = "%s:" % self.__class__.__name__
1658         text = text + "\n\tdn=%s" % self.site_dnstr
1659         text = text + "\n\toptions=0x%X" % self.site_options
1660         text = text + "\n\ttopo_generator=%s" % self.site_topo_generator
1661         text = text + "\n\ttopo_failover=%d" % self.site_topo_failover
1662         for key, dsa in self.dsa_table.items():
1663             text = text + "\n%s" % dsa
1664         return text
1665
1666
1667 class GraphNode(object):
1668     """A graph node describing a set of edges that should be directed to it.
1669
1670     Each edge is a connection for a particular naming context replica directed
1671     from another node in the forest to this node.
1672     """
1673
1674     def __init__(self, dsa_dnstr, max_node_edges):
1675         """Instantiate the graph node according to a DSA dn string
1676
1677         :param max_node_edges: maximum number of edges that should ever
1678             be directed to the node
1679         """
1680         self.max_edges = max_node_edges
1681         self.dsa_dnstr = dsa_dnstr
1682         self.edge_from = []
1683
1684     def __str__(self):
1685         text = "%s:" % self.__class__.__name__
1686         text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
1687         text = text + "\n\tmax_edges=%d" % self.max_edges
1688
1689         for i, edge in enumerate(self.edge_from):
1690             text = text + "\n\tedge_from[%d]=%s" % (i, edge)
1691         return text
1692
1693     def add_edge_from(self, from_dsa_dnstr):
1694         """Add an edge from the dsa to our graph nodes edge from list
1695
1696         :param from_dsa_dnstr: the dsa that the edge emanates from
1697         """
1698         assert from_dsa_dnstr is not None
1699
1700         # No edges from myself to myself
1701         if from_dsa_dnstr == self.dsa_dnstr:
1702             return False
1703         # Only one edge from a particular node
1704         if from_dsa_dnstr in self.edge_from:
1705             return False
1706         # Not too many edges
1707         if len(self.edge_from) >= self.max_edges:
1708             return False
1709         self.edge_from.append(from_dsa_dnstr)
1710         return True
1711
1712     def add_edges_from_connections(self, dsa):
1713         """For each nTDSConnection object associated with a particular
1714         DSA, we test if it implies an edge to this graph node (i.e.
1715         the "fromServer" attribute).  If it does then we add an
1716         edge from the server unless we are over the max edges for this
1717         graph node
1718
1719         :param dsa: dsa with a dnstr equivalent to his graph node
1720         """
1721         for connect in dsa.connect_table.values():
1722             self.add_edge_from(connect.from_dnstr)
1723
1724     def add_connections_from_edges(self, dsa):
1725         """For each edge directed to this graph node, ensure there
1726            is a corresponding nTDSConnection object in the dsa.
1727         """
1728         for edge_dnstr in self.edge_from:
1729             connections = dsa.get_connection_by_from_dnstr(edge_dnstr)
1730
1731             # For each edge directed to the NC replica that
1732             # "should be present" on the local DC, the KCC determines
1733             # whether an object c exists such that:
1734             #
1735             #    c is a child of the DC's nTDSDSA object.
1736             #    c.objectCategory = nTDSConnection
1737             #
1738             # Given the NC replica ri from which the edge is directed,
1739             #    c.fromServer is the dsname of the nTDSDSA object of
1740             #    the DC on which ri "is present".
1741             #
1742             #    c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1743
1744             found_valid = False
1745             for connect in connections:
1746                 if connect.is_rodc_topology():
1747                     continue
1748                 found_valid = True
1749
1750             if found_valid:
1751                 continue
1752
1753             # if no such object exists then the KCC adds an object
1754             # c with the following attributes
1755
1756             # Generate a new dnstr for this nTDSConnection
1757             opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1758             flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1759                      dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1760
1761             dsa.new_connection(opt, flags, None, edge_dnstr, None)
1762
1763     def has_sufficient_edges(self):
1764         '''Return True if we have met the maximum "from edges" criteria'''
1765         if len(self.edge_from) >= self.max_edges:
1766             return True
1767         return False
1768
1769
1770 class Transport(object):
1771     """Class defines a Inter-site transport found under Sites
1772     """
1773
1774     def __init__(self, dnstr):
1775         self.dnstr = dnstr
1776         self.options = 0
1777         self.guid = None
1778         self.name = None
1779         self.address_attr = None
1780         self.bridgehead_list = []
1781
1782     def __str__(self):
1783         '''Debug dump string output of Transport object'''
1784
1785         text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
1786         text = text + "\n\tguid=%s" % str(self.guid)
1787         text = text + "\n\toptions=%d" % self.options
1788         text = text + "\n\taddress_attr=%s" % self.address_attr
1789         text = text + "\n\tname=%s" % self.name
1790         for dnstr in self.bridgehead_list:
1791             text = text + "\n\tbridgehead_list=%s" % dnstr
1792
1793         return text
1794
1795     def load_transport(self, samdb):
1796         """Given a Transport object with an prior initialization
1797         for the object's DN, search for the DN and load attributes
1798         from the samdb.
1799         """
1800         attrs = ["objectGUID",
1801                  "options",
1802                  "name",
1803                  "bridgeheadServerListBL",
1804                  "transportAddressAttribute"]
1805         try:
1806             res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
1807                                attrs=attrs)
1808
1809         except ldb.LdbError, (enum, estr):
1810             raise Exception("Unable to find Transport for (%s) - (%s)" %
1811                             (self.dnstr, estr))
1812
1813         msg = res[0]
1814         self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
1815                               msg["objectGUID"][0]))
1816
1817         if "options" in msg:
1818             self.options = int(msg["options"][0])
1819
1820         if "transportAddressAttribute" in msg:
1821             self.address_attr = str(msg["transportAddressAttribute"][0])
1822
1823         if "name" in msg:
1824             self.name = str(msg["name"][0])
1825
1826         if "bridgeheadServerListBL" in msg:
1827             for value in msg["bridgeheadServerListBL"]:
1828                 dsdn = dsdb_Dn(samdb, value)
1829                 dnstr = str(dsdn.dn)
1830                 if dnstr not in self.bridgehead_list:
1831                     self.bridgehead_list.append(dnstr)
1832
1833
1834 class RepsFromTo(object):
1835     """Class encapsulation of the NDR repsFromToBlob.
1836
1837     Removes the necessity of external code having to
1838     understand about other_info or manipulation of
1839     update flags.
1840     """
1841     def __init__(self, nc_dnstr=None, ndr_blob=None):
1842
1843         self.__dict__['to_be_deleted'] = False
1844         self.__dict__['nc_dnstr'] = nc_dnstr
1845         self.__dict__['update_flags'] = 0x0
1846         # XXX the following sounds dubious and/or better solved
1847         # elsewhere, but lets leave it for now. In particular, there
1848         # seems to be no reason for all the non-ndr generated
1849         # attributes to be handled in the round about way (e.g.
1850         # self.__dict__['to_be_deleted'] = False above). On the other
1851         # hand, it all seems to work. Hooray! Hands off!.
1852         #
1853         # WARNING:
1854         #
1855         # There is a very subtle bug here with python
1856         # and our NDR code.  If you assign directly to
1857         # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1858         # then a proper python GC reference count is not
1859         # maintained.
1860         #
1861         # To work around this we maintain an internal
1862         # reference to "dns_name(x)" and "other_info" elements
1863         # of repsFromToBlob.  This internal reference
1864         # is hidden within this class but it is why you
1865         # see statements like this below:
1866         #
1867         #   self.__dict__['ndr_blob'].ctr.other_info = \
1868         #        self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1869         #
1870         # That would appear to be a redundant assignment but
1871         # it is necessary to hold a proper python GC reference
1872         # count.
1873         if ndr_blob is None:
1874             self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
1875             self.__dict__['ndr_blob'].version = 0x1
1876             self.__dict__['dns_name1'] = None
1877             self.__dict__['dns_name2'] = None
1878
1879             self.__dict__['ndr_blob'].ctr.other_info = \
1880                 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1881
1882         else:
1883             self.__dict__['ndr_blob'] = ndr_blob
1884             self.__dict__['other_info'] = ndr_blob.ctr.other_info
1885
1886             if ndr_blob.version == 0x1:
1887                 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
1888                 self.__dict__['dns_name2'] = None
1889             else:
1890                 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
1891                 self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
1892
1893     def __str__(self):
1894         '''Debug dump string output of class'''
1895
1896         text = "%s:" % self.__class__.__name__
1897         text += "\n\tdnstr=%s" % self.nc_dnstr
1898         text += "\n\tupdate_flags=0x%X" % self.update_flags
1899         text += "\n\tversion=%d" % self.version
1900         text += "\n\tsource_dsa_obj_guid=%s" % self.source_dsa_obj_guid
1901         text += ("\n\tsource_dsa_invocation_id=%s" %
1902                  self.source_dsa_invocation_id)
1903         text += "\n\ttransport_guid=%s" % self.transport_guid
1904         text += "\n\treplica_flags=0x%X" % self.replica_flags
1905         text += ("\n\tconsecutive_sync_failures=%d" %
1906                  self.consecutive_sync_failures)
1907         text += "\n\tlast_success=%s" % self.last_success
1908         text += "\n\tlast_attempt=%s" % self.last_attempt
1909         text += "\n\tdns_name1=%s" % self.dns_name1
1910         text += "\n\tdns_name2=%s" % self.dns_name2
1911         text += "\n\tschedule[ "
1912         for slot in self.schedule:
1913             text += "0x%X " % slot
1914         text += "]"
1915
1916         return text
1917
1918     def __setattr__(self, item, value):
1919
1920         if item in ['schedule', 'replica_flags', 'transport_guid',
1921                     'source_dsa_obj_guid', 'source_dsa_invocation_id',
1922                     'consecutive_sync_failures', 'last_success',
1923                     'last_attempt']:
1924
1925             if item in ['replica_flags']:
1926                 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
1927             elif item in ['schedule']:
1928                 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
1929
1930             setattr(self.__dict__['ndr_blob'].ctr, item, value)
1931
1932         elif item in ['dns_name1']:
1933             self.__dict__['dns_name1'] = value
1934
1935             if self.__dict__['ndr_blob'].version == 0x1:
1936                 self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
1937                     self.__dict__['dns_name1']
1938             else:
1939                 self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
1940                     self.__dict__['dns_name1']
1941
1942         elif item in ['dns_name2']:
1943             self.__dict__['dns_name2'] = value
1944
1945             if self.__dict__['ndr_blob'].version == 0x1:
1946                 raise AttributeError(item)
1947             else:
1948                 self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
1949                     self.__dict__['dns_name2']
1950
1951         elif item in ['nc_dnstr']:
1952             self.__dict__['nc_dnstr'] = value
1953
1954         elif item in ['to_be_deleted']:
1955             self.__dict__['to_be_deleted'] = value
1956
1957         elif item in ['version']:
1958             raise AttributeError("Attempt to set readonly attribute %s" % item)
1959         else:
1960             raise AttributeError("Unknown attribute %s" % item)
1961
1962         self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
1963
1964     def __getattr__(self, item):
1965         """Overload of RepsFromTo attribute retrieval.
1966
1967         Allows external code to ignore substructures within the blob
1968         """
1969         if item in ['schedule', 'replica_flags', 'transport_guid',
1970                     'source_dsa_obj_guid', 'source_dsa_invocation_id',
1971                     'consecutive_sync_failures', 'last_success',
1972                     'last_attempt']:
1973             return getattr(self.__dict__['ndr_blob'].ctr, item)
1974
1975         elif item in ['version']:
1976             return self.__dict__['ndr_blob'].version
1977
1978         elif item in ['dns_name1']:
1979             if self.__dict__['ndr_blob'].version == 0x1:
1980                 return self.__dict__['ndr_blob'].ctr.other_info.dns_name
1981             else:
1982                 return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
1983
1984         elif item in ['dns_name2']:
1985             if self.__dict__['ndr_blob'].version == 0x1:
1986                 raise AttributeError(item)
1987             else:
1988                 return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
1989
1990         elif item in ['to_be_deleted']:
1991             return self.__dict__['to_be_deleted']
1992
1993         elif item in ['nc_dnstr']:
1994             return self.__dict__['nc_dnstr']
1995
1996         elif item in ['update_flags']:
1997             return self.__dict__['update_flags']
1998
1999         raise AttributeError("Unknown attribute %s" % item)
2000
2001     def is_modified(self):
2002         return (self.update_flags != 0x0)
2003
2004     def set_unmodified(self):
2005         self.__dict__['update_flags'] = 0x0
2006
2007
2008 class SiteLink(object):
2009     """Class defines a site link found under sites
2010     """
2011
2012     def __init__(self, dnstr):
2013         self.dnstr = dnstr
2014         self.options = 0
2015         self.system_flags = 0
2016         self.cost = 0
2017         self.schedule = None
2018         self.interval = None
2019         self.site_list = []
2020
2021     def __str__(self):
2022         '''Debug dump string output of Transport object'''
2023
2024         text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
2025         text = text + "\n\toptions=%d" % self.options
2026         text = text + "\n\tsystem_flags=%d" % self.system_flags
2027         text = text + "\n\tcost=%d" % self.cost
2028         text = text + "\n\tinterval=%s" % self.interval
2029
2030         if self.schedule is not None:
2031             text += "\n\tschedule.size=%s" % self.schedule.size
2032             text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
2033             text += ("\n\tschedule.numberOfSchedules=%s" %
2034                      self.schedule.numberOfSchedules)
2035
2036             for i, header in enumerate(self.schedule.headerArray):
2037                 text += ("\n\tschedule.headerArray[%d].type=%d" %
2038                          (i, header.type))
2039                 text += ("\n\tschedule.headerArray[%d].offset=%d" %
2040                          (i, header.offset))
2041                 text = text + "\n\tschedule.dataArray[%d].slots[ " % i
2042                 for slot in self.schedule.dataArray[i].slots:
2043                     text = text + "0x%X " % slot
2044                 text = text + "]"
2045
2046         for dnstr in self.site_list:
2047             text = text + "\n\tsite_list=%s" % dnstr
2048         return text
2049
2050     def load_sitelink(self, samdb):
2051         """Given a siteLink object with an prior initialization
2052         for the object's DN, search for the DN and load attributes
2053         from the samdb.
2054         """
2055         attrs = ["options",
2056                  "systemFlags",
2057                  "cost",
2058                  "schedule",
2059                  "replInterval",
2060                  "siteList"]
2061         try:
2062             res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
2063                                attrs=attrs, controls=['extended_dn:0'])
2064
2065         except ldb.LdbError, (enum, estr):
2066             raise Exception("Unable to find SiteLink for (%s) - (%s)" %
2067                             (self.dnstr, estr))
2068
2069         msg = res[0]
2070
2071         if "options" in msg:
2072             self.options = int(msg["options"][0])
2073
2074         if "systemFlags" in msg:
2075             self.system_flags = int(msg["systemFlags"][0])
2076
2077         if "cost" in msg:
2078             self.cost = int(msg["cost"][0])
2079
2080         if "replInterval" in msg:
2081             self.interval = int(msg["replInterval"][0])
2082
2083         if "siteList" in msg:
2084             for value in msg["siteList"]:
2085                 dsdn = dsdb_Dn(samdb, value)
2086                 guid = misc.GUID(dsdn.dn.get_extended_component('GUID'))
2087                 if guid not in self.site_list:
2088                     self.site_list.append(guid)
2089
2090         if "schedule" in msg:
2091             self.schedule = ndr_unpack(drsblobs.schedule, value)
2092         else:
2093             self.schedule = new_connection_schedule()
2094
2095
2096 class KCCFailedObject(object):
2097     def __init__(self, uuid, failure_count, time_first_failure,
2098                  last_result, dns_name):
2099         self.uuid = uuid
2100         self.failure_count = failure_count
2101         self.time_first_failure = time_first_failure
2102         self.last_result = last_result
2103         self.dns_name = dns_name
2104
2105
2106 class VertexColor(object):
2107     (red, black, white, unknown) = range(0, 4)
2108
2109
2110 class Vertex(object):
2111     """Class encapsulation of a Site Vertex in the
2112     intersite topology replication algorithm
2113     """
2114     def __init__(self, site, part):
2115         self.site = site
2116         self.part = part
2117         self.color = VertexColor.unknown
2118         self.edges = []
2119         self.accept_red_red = []
2120         self.accept_black = []
2121         self.repl_info = ReplInfo()
2122         self.root = self
2123         self.guid = None
2124         self.component_id = self
2125         self.demoted = False
2126         self.options = 0
2127         self.interval = 0
2128
2129     def color_vertex(self):
2130         """Color each vertex to indicate which kind of NC
2131         replica it contains
2132         """
2133         # IF s contains one or more DCs with full replicas of the
2134         # NC cr!nCName
2135         #    SET v.Color to COLOR.RED
2136         # ELSEIF s contains one or more partial replicas of the NC
2137         #    SET v.Color to COLOR.BLACK
2138         #ELSE
2139         #    SET v.Color to COLOR.WHITE
2140
2141         # set to minimum (no replica)
2142         self.color = VertexColor.white
2143
2144         for dnstr, dsa in self.site.dsa_table.items():
2145             rep = dsa.get_current_replica(self.part.nc_dnstr)
2146             if rep is None:
2147                 continue
2148
2149             # We have a full replica which is the largest
2150             # value so exit
2151             if not rep.is_partial():
2152                 self.color = VertexColor.red
2153                 break
2154             else:
2155                 self.color = VertexColor.black
2156
2157     def is_red(self):
2158         assert(self.color != VertexColor.unknown)
2159         return (self.color == VertexColor.red)
2160
2161     def is_black(self):
2162         assert(self.color != VertexColor.unknown)
2163         return (self.color == VertexColor.black)
2164
2165     def is_white(self):
2166         assert(self.color != VertexColor.unknown)
2167         return (self.color == VertexColor.white)
2168
2169
2170 class IntersiteGraph(object):
2171     """Graph for representing the intersite"""
2172     def __init__(self):
2173         self.vertices = set()
2174         self.edges = set()
2175         self.edge_set = set()
2176         # All vertices that are endpoints of edges
2177         self.connected_vertices = None
2178
2179
2180 class MultiEdgeSet(object):
2181     """Defines a multi edge set"""
2182     def __init__(self):
2183         self.guid = 0  # objectGuid siteLinkBridge
2184         self.edges = []
2185
2186
2187 class MultiEdge(object):
2188     def __init__(self):
2189         self.site_link = None  # object siteLink
2190         self.vertices = []
2191         self.con_type = None  # interSiteTransport GUID
2192         self.repl_info = ReplInfo()
2193         self.directed = True
2194
2195
2196 class ReplInfo(object):
2197     def __init__(self):
2198         self.cost = 0
2199         self.interval = 0
2200         self.options = 0
2201         self.schedule = None
2202
2203
2204 class InternalEdge(object):
2205     def __init__(self, v1, v2, redred, repl, eType, site_link):
2206         self.v1 = v1
2207         self.v2 = v2
2208         self.red_red = redred
2209         self.repl_info = repl
2210         self.e_type = eType
2211         self.site_link = site_link
2212
2213     def __eq__(self, other):
2214         return not self < other and not other < self
2215
2216     def __ne__(self, other):
2217         return self < other or other < self
2218
2219     def __gt__(self, other):
2220         return other < self
2221
2222     def __ge__(self, other):
2223         return not self < other
2224
2225     def __le__(self, other):
2226         return not other < self
2227
2228     # TODO compare options and interval
2229     def __lt__(self, other):
2230         if self.red_red != other.red_red:
2231             return self.red_red
2232
2233         if self.repl_info.cost != other.repl_info.cost:
2234             return self.repl_info.cost < other.repl_info.cost
2235
2236         self_time = total_schedule(self.repl_info.schedule)
2237         other_time = total_schedule(other.repl_info.schedule)
2238         if self_time != other_time:
2239             return self_time > other_time
2240
2241         #XXX guid comparison using ndr_pack
2242         if self.v1.guid != other.v1.guid:
2243             return self.v1.ndrpacked_guid < other.v1.ndrpacked_guid
2244
2245         if self.v2.guid != other.v2.guid:
2246             return self.v2.ndrpacked_guid < other.v2.ndrpacked_guid
2247
2248         return self.e_type < other.e_type
2249
2250
2251 ##################################################
2252 # Global Functions and Variables
2253 ##################################################
2254 MAX_DWORD = 2 ** 32 - 1
2255
2256
2257 def get_dsa_config_rep(dsa):
2258     # Find configuration NC replica for the DSA
2259     for c_rep in dsa.current_rep_table.values():
2260         if c_rep.is_config():
2261             return c_rep
2262
2263     raise KCCError("Unable to find config NC replica for (%s)" %
2264                    dsa.dsa_dnstr)
2265
2266
2267 def sort_dsa_by_guid(dsa1, dsa2):
2268     "use ndr_pack for GUID comparison, as appears correct in some places"""
2269     return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
2270
2271
2272 def total_schedule(schedule):
2273     if schedule is None:
2274         return 84 * 8  # 84 bytes = 84 * 8 bits
2275
2276     total = 0
2277     for byte in schedule:
2278         while byte != 0:
2279             total += byte & 1
2280             byte >>= 1
2281     return total
2282
2283
2284 # Returns true if schedule intersect
2285 def combine_repl_info(info_a, info_b, info_c):
2286     info_c.interval = max(info_a.interval, info_b.interval)
2287     info_c.options = info_a.options & info_b.options
2288
2289     if info_a.schedule is None:
2290         info_a.schedule = [0xFF] * 84
2291     if info_b.schedule is None:
2292         info_b.schedule = [0xFF] * 84
2293
2294     new_info = [0] * 84
2295     i = 0
2296     count = 0
2297     while i < 84:
2298         # Note that this operation is actually bitwise
2299         new_info = info_a.schedule[i] & info_b.schedule[i]
2300         if new_info != 0:
2301             count += 1
2302         i += 1
2303
2304     if count == 0:
2305         return False
2306
2307         info_c.schedule = new_info
2308
2309     # Truncate to MAX_DWORD
2310     info_c.cost = info_a.cost + info_b.cost
2311     if info_c.cost > MAX_DWORD:
2312         info_c.cost = MAX_DWORD
2313
2314     return True
2315
2316
2317 def convert_schedule_to_repltimes(schedule):
2318     """Convert NTDS Connection schedule to replTime schedule.
2319
2320     Schedule defined in  MS-ADTS 6.1.4.5.2
2321     ReplTimes defined in MS-DRSR 5.164.
2322
2323     "Schedule" has 168 bytes but only the lower nibble of each is
2324     significant. There is one byte per hour. Bit 3 (0x08) represents
2325     the first 15 minutes of the hour and bit 0 (0x01) represents the
2326     last 15 minutes. The first byte presumably covers 12am - 1am
2327     Sunday, though the spec doesn't define the start of a week.
2328
2329     "ReplTimes" has 84 bytes which are the 168 lower nibbles of
2330     "Schedule" packed together. Thus each byte covers 2 hours. Bits 7
2331     (i.e. 0x80) is the first 15 minutes and bit 0 is the last. The
2332     first byte covers Sunday 12am - 2am (per spec).
2333
2334     Here we pack two elements of the NTDS Connection schedule slots
2335     into one element of the replTimes list.
2336
2337     If no schedule appears in NTDS Connection then a default of 0x11
2338     is set in each replTimes slot as per behaviour noted in a Windows
2339     DC. That default would cause replication within the last 15
2340     minutes of each hour.
2341     """
2342     if schedule is None or schedule.dataArray[0] is None:
2343         return [0x11] * 84
2344
2345     times = []
2346     data = schedule.dataArray[0].slots
2347
2348     for i in range(84):
2349         times.append((data[i * 2] & 0xF) << 4 | (data[i * 2 + 1] & 0xF))
2350
2351     return times
2352
2353
2354 def new_connection_schedule():
2355     """Create a default schedule for an NTDSConnection or Sitelink. This
2356     is packed differently from the repltimes schedule used elsewhere
2357     in KCC (where the 168 nibbles are packed into 84 bytes).
2358     """
2359     # 168 byte instances of the 0x01 value.  The low order 4 bits
2360     # of the byte equate to 15 minute intervals within a single hour.
2361     # There are 168 bytes because there are 168 hours in a full week
2362     # Effectively we are saying to perform replication at the end of
2363     # each hour of the week
2364     schedule = drsblobs.schedule()
2365
2366     schedule.size = 188
2367     schedule.bandwidth = 0
2368     schedule.numberOfSchedules = 1
2369
2370     header = drsblobs.scheduleHeader()
2371     header.type = 0
2372     header.offset = 20
2373
2374     schedule.headerArray = [header]
2375
2376     data = drsblobs.scheduleSlots()
2377     data.slots = [0x01] * 168
2378
2379     schedule.dataArray = [data]
2380     return schedule