c58d5d23217a0563d9250bebad52065f860ca7e1
[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.decode('utf8'))
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].decode('utf8'))
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].decode('utf8'))
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.decode('utf8'))
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(
1585             self.rw_dsa_table.values(),
1586             key=lambda dsa: ndr_pack(dsa.dsa_guid))
1587
1588         # double word number of 100 nanosecond intervals since 1600s
1589
1590         # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1591         # if o!interSiteTopologyFailover is 0 or has no value.
1592         #
1593         # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1594         #       so it appears we have to turn f into the same interval
1595         #
1596         #       interSiteTopologyFailover (if set) appears to be in minutes
1597         #       so we'll need to convert to senconds and then 100 nanosecond
1598         #       intervals
1599         #       XXX [MS-ADTS] 6.2.2.3.1 says it is seconds, not minutes.
1600         #
1601         #       10,000,000 is number of 100 nanosecond intervals in a second
1602         if self.site_topo_failover == 0:
1603             f = 2 * 60 * 60 * 10000000
1604         else:
1605             f = self.site_topo_failover * 60 * 10000000
1606
1607         # Let o be the site settings object for the site of the local
1608         # DC, or NULL if no such o exists.
1609         d_dsa = self.dsa_table.get(self.site_topo_generator)
1610
1611         # From MS-ADTS 6.2.2.3.1 ISTG selection:
1612         #     If o != NULL and o!interSiteTopologyGenerator is not the
1613         #     nTDSDSA object for the local DC and
1614         #     o!interSiteTopologyGenerator is an element dj of sequence D:
1615         #
1616         if d_dsa is not None and d_dsa is not mydsa:
1617             # From MS-ADTS 6.2.2.3.1 ISTG Selection:
1618             #     Let c be the cursor in the replUpToDateVector variable
1619             #     associated with the NC replica of the config NC such
1620             #     that c.uuidDsa = dj!invocationId. If no such c exists
1621             #     (No evidence of replication from current ITSG):
1622             #         Let i = j.
1623             #         Let t = 0.
1624             #
1625             #     Else if the current time < c.timeLastSyncSuccess - f
1626             #     (Evidence of time sync problem on current ISTG):
1627             #         Let i = 0.
1628             #         Let t = 0.
1629             #
1630             #     Else (Evidence of replication from current ITSG):
1631             #         Let i = j.
1632             #         Let t = c.timeLastSyncSuccess.
1633             #
1634             # last_success appears to be a double word containing
1635             #     number of 100 nanosecond intervals since the 1600s
1636             j_idx = D_sort.index(d_dsa)
1637
1638             found = False
1639             for cursor in c_rep.rep_replUpToDateVector_cursors:
1640                 if d_dsa.dsa_ivid == cursor.source_dsa_invocation_id:
1641                     found = True
1642                     break
1643
1644             if not found:
1645                 i_idx = j_idx
1646                 t_time = 0
1647
1648             #XXX doc says current time < c.timeLastSyncSuccess - f
1649             # which is true only if f is negative or clocks are wrong.
1650             # f is not negative in the default case (2 hours).
1651             elif self.nt_now - cursor.last_sync_success > f:
1652                 i_idx = 0
1653                 t_time = 0
1654             else:
1655                 i_idx = j_idx
1656                 t_time = cursor.last_sync_success
1657
1658         # Otherwise (Nominate local DC as ISTG):
1659         #     Let i be the integer such that di is the nTDSDSA
1660         #         object for the local DC.
1661         #     Let t = the current time.
1662         else:
1663             i_idx = D_sort.index(mydsa)
1664             t_time = self.nt_now
1665
1666         # Compute a function that maintains the current ISTG if
1667         # it is alive, cycles through other candidates if not.
1668         #
1669         # Let k be the integer (i + ((current time - t) /
1670         #     o!interSiteTopologyFailover)) MOD |D|.
1671         #
1672         # Note: We don't want to divide by zero here so they must
1673         #       have meant "f" instead of "o!interSiteTopologyFailover"
1674         k_idx = (i_idx + ((self.nt_now - t_time) // f)) % len(D_sort)
1675
1676         # The local writable DC acts as an ISTG for its site if and
1677         # only if dk is the nTDSDSA object for the local DC. If the
1678         # local DC does not act as an ISTG, the KCC skips the
1679         # remainder of this task.
1680         d_dsa = D_sort[k_idx]
1681         d_dsa.dsa_is_istg = True
1682
1683         # Update if we are the ISTG, otherwise return
1684         if d_dsa is not mydsa:
1685             return False
1686
1687         # Nothing to do
1688         if self.site_topo_generator == mydsa.dsa_dnstr:
1689             return True
1690
1691         self.site_topo_generator = mydsa.dsa_dnstr
1692
1693         # If readonly database then do not perform a
1694         # persistent update
1695         if ro:
1696             return True
1697
1698         # Perform update to the samdb
1699         ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1700
1701         m = ldb.Message()
1702         m.dn = ldb.Dn(samdb, ssdn)
1703
1704         m["interSiteTopologyGenerator"] = \
1705             ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
1706                                "interSiteTopologyGenerator")
1707         try:
1708             samdb.modify(m)
1709
1710         except ldb.LdbError as estr:
1711             raise KCCError(
1712                 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1713                 (ssdn, estr))
1714         return True
1715
1716     def is_intrasite_topology_disabled(self):
1717         '''Returns True if intra-site topology is disabled for site'''
1718         return (self.site_options &
1719                 dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0
1720
1721     def is_intersite_topology_disabled(self):
1722         '''Returns True if inter-site topology is disabled for site'''
1723         return ((self.site_options &
1724                  dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED)
1725                 != 0)
1726
1727     def is_random_bridgehead_disabled(self):
1728         '''Returns True if selection of random bridgehead is disabled'''
1729         return (self.site_options &
1730                 dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0
1731
1732     def is_detect_stale_disabled(self):
1733         '''Returns True if detect stale is disabled for site'''
1734         return (self.site_options &
1735                 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0
1736
1737     def is_cleanup_ntdsconn_disabled(self):
1738         '''Returns True if NTDS Connection cleanup is disabled for site'''
1739         return (self.site_options &
1740                 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0
1741
1742     def same_site(self, dsa):
1743         '''Return True if dsa is in this site'''
1744         if self.get_dsa(dsa.dsa_dnstr):
1745             return True
1746         return False
1747
1748     def is_rodc_site(self):
1749         if len(self.dsa_table) > 0 and len(self.rw_dsa_table) == 0:
1750             return True
1751         return False
1752
1753     def __str__(self):
1754         '''Debug dump string output of class'''
1755         text = "%s:" % self.__class__.__name__
1756         text = text + "\n\tdn=%s" % self.site_dnstr
1757         text = text + "\n\toptions=0x%X" % self.site_options
1758         text = text + "\n\ttopo_generator=%s" % self.site_topo_generator
1759         text = text + "\n\ttopo_failover=%d" % self.site_topo_failover
1760         for key, dsa in self.dsa_table.items():
1761             text = text + "\n%s" % dsa
1762         return text
1763
1764
1765 class GraphNode(object):
1766     """A graph node describing a set of edges that should be directed to it.
1767
1768     Each edge is a connection for a particular naming context replica directed
1769     from another node in the forest to this node.
1770     """
1771
1772     def __init__(self, dsa_dnstr, max_node_edges):
1773         """Instantiate the graph node according to a DSA dn string
1774
1775         :param max_node_edges: maximum number of edges that should ever
1776             be directed to the node
1777         """
1778         self.max_edges = max_node_edges
1779         self.dsa_dnstr = dsa_dnstr
1780         self.edge_from = []
1781
1782     def __str__(self):
1783         text = "%s:" % self.__class__.__name__
1784         text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
1785         text = text + "\n\tmax_edges=%d" % self.max_edges
1786
1787         for i, edge in enumerate(self.edge_from):
1788             if isinstance(edge, str):
1789                 text += "\n\tedge_from[%d]=%s" % (i, edge)
1790
1791         return text
1792
1793     def add_edge_from(self, from_dsa_dnstr):
1794         """Add an edge from the dsa to our graph nodes edge from list
1795
1796         :param from_dsa_dnstr: the dsa that the edge emanates from
1797         """
1798         assert isinstance(from_dsa_dnstr, str)
1799
1800         # No edges from myself to myself
1801         if from_dsa_dnstr == self.dsa_dnstr:
1802             return False
1803         # Only one edge from a particular node
1804         if from_dsa_dnstr in self.edge_from:
1805             return False
1806         # Not too many edges
1807         if len(self.edge_from) >= self.max_edges:
1808             return False
1809         self.edge_from.append(from_dsa_dnstr)
1810         return True
1811
1812     def add_edges_from_connections(self, dsa):
1813         """For each nTDSConnection object associated with a particular
1814         DSA, we test if it implies an edge to this graph node (i.e.
1815         the "fromServer" attribute).  If it does then we add an
1816         edge from the server unless we are over the max edges for this
1817         graph node
1818
1819         :param dsa: dsa with a dnstr equivalent to his graph node
1820         """
1821         for connect in dsa.connect_table.values():
1822             self.add_edge_from(connect.from_dnstr)
1823
1824     def add_connections_from_edges(self, dsa, transport):
1825         """For each edge directed to this graph node, ensure there
1826            is a corresponding nTDSConnection object in the dsa.
1827         """
1828         for edge_dnstr in self.edge_from:
1829             connections = dsa.get_connection_by_from_dnstr(edge_dnstr)
1830
1831             # For each edge directed to the NC replica that
1832             # "should be present" on the local DC, the KCC determines
1833             # whether an object c exists such that:
1834             #
1835             #    c is a child of the DC's nTDSDSA object.
1836             #    c.objectCategory = nTDSConnection
1837             #
1838             # Given the NC replica ri from which the edge is directed,
1839             #    c.fromServer is the dsname of the nTDSDSA object of
1840             #    the DC on which ri "is present".
1841             #
1842             #    c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1843
1844             found_valid = False
1845             for connect in connections:
1846                 if connect.is_rodc_topology():
1847                     continue
1848                 found_valid = True
1849
1850             if found_valid:
1851                 continue
1852
1853             # if no such object exists then the KCC adds an object
1854             # c with the following attributes
1855
1856             # Generate a new dnstr for this nTDSConnection
1857             opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1858             flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1859                      dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1860
1861             dsa.new_connection(opt, flags, transport, edge_dnstr, None)
1862
1863     def has_sufficient_edges(self):
1864         '''Return True if we have met the maximum "from edges" criteria'''
1865         if len(self.edge_from) >= self.max_edges:
1866             return True
1867         return False
1868
1869
1870 class Transport(object):
1871     """Class defines a Inter-site transport found under Sites
1872     """
1873
1874     def __init__(self, dnstr):
1875         self.dnstr = dnstr
1876         self.options = 0
1877         self.guid = None
1878         self.name = None
1879         self.address_attr = None
1880         self.bridgehead_list = []
1881
1882     def __str__(self):
1883         '''Debug dump string output of Transport object'''
1884
1885         text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
1886         text = text + "\n\tguid=%s" % str(self.guid)
1887         text = text + "\n\toptions=%d" % self.options
1888         text = text + "\n\taddress_attr=%s" % self.address_attr
1889         text = text + "\n\tname=%s" % self.name
1890         for dnstr in self.bridgehead_list:
1891             text = text + "\n\tbridgehead_list=%s" % dnstr
1892
1893         return text
1894
1895     def load_transport(self, samdb):
1896         """Given a Transport object with an prior initialization
1897         for the object's DN, search for the DN and load attributes
1898         from the samdb.
1899         """
1900         attrs = ["objectGUID",
1901                  "options",
1902                  "name",
1903                  "bridgeheadServerListBL",
1904                  "transportAddressAttribute"]
1905         try:
1906             res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
1907                                attrs=attrs)
1908
1909         except ldb.LdbError as e18:
1910             (enum, estr) = e18.args
1911             raise KCCError("Unable to find Transport for (%s) - (%s)" %
1912                            (self.dnstr, estr))
1913
1914         msg = res[0]
1915         self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
1916                               msg["objectGUID"][0]))
1917
1918         if "options" in msg:
1919             self.options = int(msg["options"][0])
1920
1921         if "transportAddressAttribute" in msg:
1922             self.address_attr = str(msg["transportAddressAttribute"][0])
1923
1924         if "name" in msg:
1925             self.name = str(msg["name"][0])
1926
1927         if "bridgeheadServerListBL" in msg:
1928             for value in msg["bridgeheadServerListBL"]:
1929                 dsdn = dsdb_Dn(samdb, value.decode('utf8'))
1930                 dnstr = str(dsdn.dn)
1931                 if dnstr not in self.bridgehead_list:
1932                     self.bridgehead_list.append(dnstr)
1933
1934
1935 class RepsFromTo(object):
1936     """Class encapsulation of the NDR repsFromToBlob.
1937
1938     Removes the necessity of external code having to
1939     understand about other_info or manipulation of
1940     update flags.
1941     """
1942     def __init__(self, nc_dnstr=None, ndr_blob=None):
1943
1944         self.__dict__['to_be_deleted'] = False
1945         self.__dict__['nc_dnstr'] = nc_dnstr
1946         self.__dict__['update_flags'] = 0x0
1947         # XXX the following sounds dubious and/or better solved
1948         # elsewhere, but lets leave it for now. In particular, there
1949         # seems to be no reason for all the non-ndr generated
1950         # attributes to be handled in the round about way (e.g.
1951         # self.__dict__['to_be_deleted'] = False above). On the other
1952         # hand, it all seems to work. Hooray! Hands off!.
1953         #
1954         # WARNING:
1955         #
1956         # There is a very subtle bug here with python
1957         # and our NDR code.  If you assign directly to
1958         # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1959         # then a proper python GC reference count is not
1960         # maintained.
1961         #
1962         # To work around this we maintain an internal
1963         # reference to "dns_name(x)" and "other_info" elements
1964         # of repsFromToBlob.  This internal reference
1965         # is hidden within this class but it is why you
1966         # see statements like this below:
1967         #
1968         #   self.__dict__['ndr_blob'].ctr.other_info = \
1969         #        self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1970         #
1971         # That would appear to be a redundant assignment but
1972         # it is necessary to hold a proper python GC reference
1973         # count.
1974         if ndr_blob is None:
1975             self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
1976             self.__dict__['ndr_blob'].version = 0x1
1977             self.__dict__['dns_name1'] = None
1978             self.__dict__['dns_name2'] = None
1979
1980             self.__dict__['ndr_blob'].ctr.other_info = \
1981                 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1982
1983         else:
1984             self.__dict__['ndr_blob'] = ndr_blob
1985             self.__dict__['other_info'] = ndr_blob.ctr.other_info
1986
1987             if ndr_blob.version == 0x1:
1988                 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
1989                 self.__dict__['dns_name2'] = None
1990             else:
1991                 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
1992                 self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
1993
1994     def __str__(self):
1995         '''Debug dump string output of class'''
1996
1997         text = "%s:" % self.__class__.__name__
1998         text += "\n\tdnstr=%s" % self.nc_dnstr
1999         text += "\n\tupdate_flags=0x%X" % self.update_flags
2000         text += "\n\tversion=%d" % self.version
2001         text += "\n\tsource_dsa_obj_guid=%s" % self.source_dsa_obj_guid
2002         text += ("\n\tsource_dsa_invocation_id=%s" %
2003                  self.source_dsa_invocation_id)
2004         text += "\n\ttransport_guid=%s" % self.transport_guid
2005         text += "\n\treplica_flags=0x%X" % self.replica_flags
2006         text += ("\n\tconsecutive_sync_failures=%d" %
2007                  self.consecutive_sync_failures)
2008         text += "\n\tlast_success=%s" % self.last_success
2009         text += "\n\tlast_attempt=%s" % self.last_attempt
2010         text += "\n\tdns_name1=%s" % self.dns_name1
2011         text += "\n\tdns_name2=%s" % self.dns_name2
2012         text += "\n\tschedule[ "
2013         for slot in self.schedule:
2014             text += "0x%X " % slot
2015         text += "]"
2016
2017         return text
2018
2019     def __setattr__(self, item, value):
2020         """Set an attribute and chyange update flag.
2021
2022         Be aware that setting any RepsFromTo attribute will set the
2023         drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS update flag.
2024         """
2025         if item in ['schedule', 'replica_flags', 'transport_guid',
2026                     'source_dsa_obj_guid', 'source_dsa_invocation_id',
2027                     'consecutive_sync_failures', 'last_success',
2028                     'last_attempt']:
2029
2030             if item in ['replica_flags']:
2031                 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
2032             elif item in ['schedule']:
2033                 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
2034
2035             setattr(self.__dict__['ndr_blob'].ctr, item, value)
2036
2037         elif item in ['dns_name1']:
2038             self.__dict__['dns_name1'] = value
2039
2040             if self.__dict__['ndr_blob'].version == 0x1:
2041                 self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
2042                     self.__dict__['dns_name1']
2043             else:
2044                 self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
2045                     self.__dict__['dns_name1']
2046
2047         elif item in ['dns_name2']:
2048             self.__dict__['dns_name2'] = value
2049
2050             if self.__dict__['ndr_blob'].version == 0x1:
2051                 raise AttributeError(item)
2052             else:
2053                 self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
2054                     self.__dict__['dns_name2']
2055
2056         elif item in ['nc_dnstr']:
2057             self.__dict__['nc_dnstr'] = value
2058
2059         elif item in ['to_be_deleted']:
2060             self.__dict__['to_be_deleted'] = value
2061
2062         elif item in ['version']:
2063             raise AttributeError("Attempt to set readonly attribute %s" % item)
2064         else:
2065             raise AttributeError("Unknown attribute %s" % item)
2066
2067         self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
2068
2069     def __getattr__(self, item):
2070         """Overload of RepsFromTo attribute retrieval.
2071
2072         Allows external code to ignore substructures within the blob
2073         """
2074         if item in ['schedule', 'replica_flags', 'transport_guid',
2075                     'source_dsa_obj_guid', 'source_dsa_invocation_id',
2076                     'consecutive_sync_failures', 'last_success',
2077                     'last_attempt']:
2078             return getattr(self.__dict__['ndr_blob'].ctr, item)
2079
2080         elif item in ['version']:
2081             return self.__dict__['ndr_blob'].version
2082
2083         elif item in ['dns_name1']:
2084             if self.__dict__['ndr_blob'].version == 0x1:
2085                 return self.__dict__['ndr_blob'].ctr.other_info.dns_name
2086             else:
2087                 return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
2088
2089         elif item in ['dns_name2']:
2090             if self.__dict__['ndr_blob'].version == 0x1:
2091                 raise AttributeError(item)
2092             else:
2093                 return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
2094
2095         elif item in ['to_be_deleted']:
2096             return self.__dict__['to_be_deleted']
2097
2098         elif item in ['nc_dnstr']:
2099             return self.__dict__['nc_dnstr']
2100
2101         elif item in ['update_flags']:
2102             return self.__dict__['update_flags']
2103
2104         raise AttributeError("Unknown attribute %s" % item)
2105
2106     def is_modified(self):
2107         return (self.update_flags != 0x0)
2108
2109     def set_unmodified(self):
2110         self.__dict__['update_flags'] = 0x0
2111
2112
2113 class SiteLink(object):
2114     """Class defines a site link found under sites
2115     """
2116
2117     def __init__(self, dnstr):
2118         self.dnstr = dnstr
2119         self.options = 0
2120         self.system_flags = 0
2121         self.cost = 0
2122         self.schedule = None
2123         self.interval = None
2124         self.site_list = []
2125
2126     def __str__(self):
2127         '''Debug dump string output of Transport object'''
2128
2129         text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
2130         text = text + "\n\toptions=%d" % self.options
2131         text = text + "\n\tsystem_flags=%d" % self.system_flags
2132         text = text + "\n\tcost=%d" % self.cost
2133         text = text + "\n\tinterval=%s" % self.interval
2134
2135         if self.schedule is not None:
2136             text += "\n\tschedule.size=%s" % self.schedule.size
2137             text += "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
2138             text += ("\n\tschedule.numberOfSchedules=%s" %
2139                      self.schedule.numberOfSchedules)
2140
2141             for i, header in enumerate(self.schedule.headerArray):
2142                 text += ("\n\tschedule.headerArray[%d].type=%d" %
2143                          (i, header.type))
2144                 text += ("\n\tschedule.headerArray[%d].offset=%d" %
2145                          (i, header.offset))
2146                 text = text + "\n\tschedule.dataArray[%d].slots[ " % i
2147                 for slot in self.schedule.dataArray[i].slots:
2148                     text = text + "0x%X " % slot
2149                 text = text + "]"
2150
2151         for guid, dn in self.site_list:
2152             text = text + "\n\tsite_list=%s (%s)" % (guid, dn)
2153         return text
2154
2155     def load_sitelink(self, samdb):
2156         """Given a siteLink object with an prior initialization
2157         for the object's DN, search for the DN and load attributes
2158         from the samdb.
2159         """
2160         attrs = ["options",
2161                  "systemFlags",
2162                  "cost",
2163                  "schedule",
2164                  "replInterval",
2165                  "siteList"]
2166         try:
2167             res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
2168                                attrs=attrs, controls=['extended_dn:0'])
2169
2170         except ldb.LdbError as e19:
2171             (enum, estr) = e19.args
2172             raise KCCError("Unable to find SiteLink for (%s) - (%s)" %
2173                            (self.dnstr, estr))
2174
2175         msg = res[0]
2176
2177         if "options" in msg:
2178             self.options = int(msg["options"][0])
2179
2180         if "systemFlags" in msg:
2181             self.system_flags = int(msg["systemFlags"][0])
2182
2183         if "cost" in msg:
2184             self.cost = int(msg["cost"][0])
2185
2186         if "replInterval" in msg:
2187             self.interval = int(msg["replInterval"][0])
2188
2189         if "siteList" in msg:
2190             for value in msg["siteList"]:
2191                 dsdn = dsdb_Dn(samdb, value.decode('utf8'))
2192                 guid = misc.GUID(dsdn.dn.get_extended_component('GUID'))
2193                 dnstr = str(dsdn.dn)
2194                 if (guid, dnstr) not in self.site_list:
2195                     self.site_list.append((guid, dnstr))
2196
2197         if "schedule" in msg:
2198             self.schedule = ndr_unpack(drsblobs.schedule, value)
2199         else:
2200             self.schedule = new_connection_schedule()
2201
2202
2203 class KCCFailedObject(object):
2204     def __init__(self, uuid, failure_count, time_first_failure,
2205                  last_result, dns_name):
2206         self.uuid = uuid
2207         self.failure_count = failure_count
2208         self.time_first_failure = time_first_failure
2209         self.last_result = last_result
2210         self.dns_name = dns_name
2211
2212
2213 ##################################################
2214 # Global Functions and Variables
2215 ##################################################
2216
2217 def get_dsa_config_rep(dsa):
2218     # Find configuration NC replica for the DSA
2219     for c_rep in dsa.current_rep_table.values():
2220         if c_rep.is_config():
2221             return c_rep
2222
2223     raise KCCError("Unable to find config NC replica for (%s)" %
2224                    dsa.dsa_dnstr)
2225
2226
2227 def new_connection_schedule():
2228     """Create a default schedule for an NTDSConnection or Sitelink. This
2229     is packed differently from the repltimes schedule used elsewhere
2230     in KCC (where the 168 nibbles are packed into 84 bytes).
2231     """
2232     # 168 byte instances of the 0x01 value.  The low order 4 bits
2233     # of the byte equate to 15 minute intervals within a single hour.
2234     # There are 168 bytes because there are 168 hours in a full week
2235     # Effectively we are saying to perform replication at the end of
2236     # each hour of the week
2237     schedule = drsblobs.schedule()
2238
2239     schedule.size = 188
2240     schedule.bandwidth = 0
2241     schedule.numberOfSchedules = 1
2242
2243     header = drsblobs.scheduleHeader()
2244     header.type = 0
2245     header.offset = 20
2246
2247     schedule.headerArray = [header]
2248
2249     data = drsblobs.scheduleSlots()
2250     data.slots = [0x01] * 168
2251
2252     schedule.dataArray = [data]
2253     return schedule
2254
2255
2256 ##################################################
2257 # DNS related calls
2258 ##################################################
2259
2260 def uncovered_sites_to_cover(samdb, site_name):
2261     """
2262     Discover which sites have no DCs and whose lowest single-hop cost
2263     distance for any link attached to that site is linked to the site supplied.
2264
2265     We compare the lowest cost of your single-hop link to this site to all of
2266     those available (if it exists). This means that a lower ranked siteLink
2267     with only the uncovered site can trump any available links (but this can
2268     only be done with specific, poorly enacted user configuration).
2269
2270     If the site is connected to more than one other site with the same
2271     siteLink, only the largest site (failing that sorted alphabetically)
2272     creates the DNS records.
2273
2274     :param samdb database
2275     :param site_name origin site (with a DC)
2276
2277     :return a list of sites this site should be covering (for DNS)
2278     """
2279     sites_to_cover = []
2280
2281     server_res = samdb.search(base=samdb.get_config_basedn(),
2282                               scope=ldb.SCOPE_SUBTREE,
2283                               expression="(&(objectClass=server)"
2284                               "(serverReference=*))")
2285
2286     site_res = samdb.search(base=samdb.get_config_basedn(),
2287                             scope=ldb.SCOPE_SUBTREE,
2288                             expression="(objectClass=site)")
2289
2290     sites_in_use = Counter()
2291     dc_count = 0
2292
2293     # Assume server is of form DC,Servers,Site-ABCD because of schema
2294     for msg in server_res:
2295         site_dn = msg.dn.parent().parent()
2296         sites_in_use[site_dn.canonical_str()] += 1
2297
2298         if site_dn.get_rdn_value().lower() == site_name.lower():
2299             dc_count += 1
2300
2301     if len(sites_in_use) != len(site_res):
2302         # There is a possible uncovered site
2303         sites_uncovered = []
2304
2305         for msg in site_res:
2306             if msg.dn.canonical_str() not in sites_in_use:
2307                 sites_uncovered.append(msg)
2308
2309         own_site_dn = "CN={},CN=Sites,{}".format(
2310             ldb.binary_encode(site_name),
2311             ldb.binary_encode(str(samdb.get_config_basedn()))
2312         )
2313
2314         for site in sites_uncovered:
2315             encoded_dn = ldb.binary_encode(str(site.dn))
2316
2317             # Get a sorted list of all siteLinks featuring the uncovered site
2318             link_res1 = samdb.search(base=samdb.get_config_basedn(),
2319                                      scope=ldb.SCOPE_SUBTREE, attrs=["cost"],
2320                                      expression="(&(objectClass=siteLink)"
2321                                      "(siteList={}))".format(encoded_dn),
2322                                      controls=["server_sort:1:0:cost"])
2323
2324             # Get a sorted list of all siteLinks connecting this an the
2325             # uncovered site
2326             link_res2 = samdb.search(base=samdb.get_config_basedn(),
2327                                      scope=ldb.SCOPE_SUBTREE,
2328                                      attrs=["cost", "siteList"],
2329                                      expression="(&(objectClass=siteLink)"
2330                                      "(siteList={})(siteList={}))".format(
2331                                          own_site_dn,
2332                                          encoded_dn),
2333                                      controls=["server_sort:1:0:cost"])
2334
2335             # Add to list if your link is equal in cost to lowest cost link
2336             if len(link_res1) > 0 and len(link_res2) > 0:
2337                 cost1 = int(link_res1[0]['cost'][0])
2338                 cost2 = int(link_res2[0]['cost'][0])
2339
2340                 # Own siteLink must match the lowest cost link
2341                 if cost1 != cost2:
2342                     continue
2343
2344                 # In a siteLink with more than 2 sites attached, only pick the
2345                 # largest site, and if there are multiple, the earliest
2346                 # alphabetically.
2347                 to_cover = True
2348                 for site_val in link_res2[0]['siteList']:
2349                     site_dn = ldb.Dn(samdb, str(site_val))
2350                     site_dn_str = site_dn.canonical_str()
2351                     site_rdn = site_dn.get_rdn_value().lower()
2352                     if sites_in_use[site_dn_str] > dc_count:
2353                         to_cover = False
2354                         break
2355                     elif (sites_in_use[site_dn_str] == dc_count and
2356                           site_rdn < site_name.lower()):
2357                         to_cover = False
2358                         break
2359
2360                 if to_cover:
2361                     site_cover_rdn = site.dn.get_rdn_value()
2362                     sites_to_cover.append(site_cover_rdn.lower())
2363
2364     return sites_to_cover