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