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