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