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