kcc: fixed tabs/spaces in kcc python implementation
[nivanova/samba-autobuild/.git] / source4 / scripting / python / samba / kcc_utils.py
1 #!/usr/bin/env python
2 #
3 # KCC topology utilities
4 #
5 # Copyright (C) Dave Craft 2011
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 import samba, ldb
21 import uuid
22
23 from samba              import dsdb
24 from samba.dcerpc       import misc
25 from samba.common       import dsdb_Dn
26
27 class NCType:
28     (unknown, schema, domain, config, application) = range(0, 5)
29
30 class NamingContext:
31     """Base class for a naming context.  Holds the DN,
32        GUID, SID (if available) and type of the DN.
33        Subclasses may inherit from this and specialize
34     """
35
36     def __init__(self, nc_dnstr, nc_guid=None, nc_sid=None):
37         """Instantiate a NamingContext
38             :param nc_dnstr: NC dn string
39             :param nc_guid: NC guid string
40             :param nc_sid: NC sid
41         """
42         self.nc_dnstr = nc_dnstr
43         self.nc_guid  = nc_guid
44         self.nc_sid   = nc_sid
45         self.nc_type  = NCType.unknown
46         return
47
48     def __str__(self):
49         '''Debug dump string output of class'''
50         return "%s:\n\tdn=%s\n\tguid=%s\n\ttype=%s" % \
51                (self.__class__.__name__, self.nc_dnstr,
52                 self.nc_guid, self.nc_type)
53
54     def is_schema(self):
55         '''Return True if NC is schema'''
56         return self.nc_type == NCType.schema
57
58     def is_domain(self):
59         '''Return True if NC is domain'''
60         return self.nc_type == NCType.domain
61
62     def is_application(self):
63         '''Return True if NC is application'''
64         return self.nc_type == NCType.application
65
66     def is_config(self):
67         '''Return True if NC is config'''
68         return self.nc_type == NCType.config
69
70     def identify_by_basedn(self, samdb):
71         """Given an NC object, identify what type is is thru
72            the samdb basedn strings and NC sid value
73         """
74         # We check against schema and config because they
75         # will be the same for all nTDSDSAs in the forest.
76         # That leaves the domain NCs which can be identified
77         # by sid and application NCs as the last identified
78         if self.nc_dnstr == str(samdb.get_schema_basedn()):
79             self.nc_type = NCType.schema
80         elif self.nc_dnstr == str(samdb.get_config_basedn()):
81             self.nc_type = NCType.config
82         elif self.nc_sid != None:
83             self.nc_type = NCType.domain
84         else:
85             self.nc_type = NCType.application
86         return
87
88     def identify_by_dsa_attr(self, samdb, attr):
89         """Given an NC which has been discovered thru the
90            nTDSDSA database object, determine what type of NC
91            it is (i.e. schema, config, domain, application) via
92            the use of the schema attribute under which the NC
93            was found.
94             :param attr: attr of nTDSDSA object where NC DN appears
95         """
96         # If the NC is listed under msDS-HasDomainNCs then
97         # this can only be a domain NC and it is our default
98         # domain for this dsa
99         if attr == "msDS-HasDomainNCs":
100             self.nc_type = NCType.domain
101
102         # If the NC is listed under hasPartialReplicaNCs
103         # this is only a domain NC
104         elif attr == "hasPartialReplicaNCs":
105             self.nc_type = NCType.domain
106
107         # NCs listed under hasMasterNCs are either
108         # default domain, schema, or config.  We
109         # utilize the identify_by_samdb_basedn() to
110         # identify those
111         elif attr == "hasMasterNCs":
112             self.identify_by_basedn(samdb)
113
114         # Still unknown (unlikely) but for completeness
115         # and for finally identifying application NCs
116         if self.nc_type == NCType.unknown:
117             self.identify_by_basedn(samdb)
118
119         return
120
121
122 class NCReplica(NamingContext):
123     """Class defines a naming context replica that is relative
124        to a specific DSA.  This is a more specific form of
125        NamingContext class (inheriting from that class) and it
126        identifies unique attributes of the DSA's replica for a NC.
127     """
128
129     def __init__(self, dsa_dnstr, dsa_guid, nc_dnstr, \
130                  nc_guid=None, nc_sid=None):
131         """Instantiate a Naming Context Replica
132             :param dsa_guid: GUID of DSA where replica appears
133             :param nc_dnstr: NC dn string
134             :param nc_guid: NC guid string
135             :param nc_sid: NC sid
136         """
137         self.rep_dsa_dnstr = dsa_dnstr
138         self.rep_dsa_guid  = dsa_guid # GUID of DSA where this appears
139         self.rep_default   = False # replica for DSA's default domain
140         self.rep_partial   = False
141         self.rep_ro        = False
142         self.rep_flags     = 0
143
144         # The (is present) test is a combination of being
145         # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or
146         # hasPartialReplicaNCs) as well as its replica flags found
147         # thru the msDS-HasInstantiatedNCs.  If the NC replica meets
148         # the first enumeration test then this flag is set true
149         self.rep_present_criteria_one = False
150
151         # Call my super class we inherited from
152         NamingContext.__init__(self, nc_dnstr, nc_guid, nc_sid)
153         return
154
155     def __str__(self):
156         '''Debug dump string output of class'''
157         text = "default=%s"  % self.rep_default + \
158                ":ro=%s"      % self.rep_ro      + \
159                ":partial=%s" % self.rep_partial + \
160                ":present=%s" % self.is_present()
161         return "%s\n\tdsaguid=%s\n\t%s" % \
162                (NamingContext.__str__(self), self.rep_dsa_guid, text)
163
164     def set_replica_flags(self, flags=None):
165         '''Set or clear NC replica flags'''
166         if (flags == None):
167             self.rep_flags = 0
168         else:
169             self.rep_flags = flags
170         return
171
172     def identify_by_dsa_attr(self, samdb, attr):
173         """Given an NC which has been discovered thru the
174            nTDSDSA database object, determine what type of NC
175            replica it is (i.e. partial, read only, default)
176             :param attr: attr of nTDSDSA object where NC DN appears
177         """
178         # If the NC was found under hasPartialReplicaNCs
179         # then a partial replica at this dsa
180         if attr == "hasPartialReplicaNCs":
181             self.rep_partial = True
182             self.rep_present_criteria_one = True
183
184         # If the NC is listed under msDS-HasDomainNCs then
185         # this can only be a domain NC and it is the DSA's
186         # default domain NC
187         elif attr == "msDS-HasDomainNCs":
188             self.rep_default = True
189
190         # NCs listed under hasMasterNCs are either
191         # default domain, schema, or config.  We check
192         # against schema and config because they will be
193         # the same for all nTDSDSAs in the forest.  That
194         # leaves the default domain NC remaining which
195         # may be different for each nTDSDSAs (and thus
196         # we don't compare agains this samdb's default
197         # basedn
198         elif attr == "hasMasterNCs":
199             self.rep_present_criteria_one = True
200
201             if self.nc_dnstr != str(samdb.get_schema_basedn()) and \
202                self.nc_dnstr != str(samdb.get_config_basedn()):
203                 self.rep_default = True
204
205         # RODC only
206         elif attr == "msDS-hasFullReplicaNCs":
207             self.rep_present_criteria_one = True
208             self.rep_ro = True
209
210         # Not RODC
211         elif attr == "msDS-hasMasterNCs":
212             self.rep_ro = False
213
214         # Now use this DSA attribute to identify the naming
215         # context type by calling the super class method
216         # of the same name
217         NamingContext.identify_by_dsa_attr(self, samdb, attr)
218         return
219
220     def is_default(self):
221         """Returns True if this is a default domain NC for the dsa
222            that this NC appears on
223         """
224         return self.rep_default
225
226     def is_ro(self):
227         '''Return True if NC replica is read only'''
228         return self.rep_ro
229
230     def is_partial(self):
231         '''Return True if NC replica is partial'''
232         return self.rep_partial
233
234     def is_present(self):
235         """Given an NC replica which has been discovered thru the
236            nTDSDSA database object and populated with replica flags
237            from the msDS-HasInstantiatedNCs; return whether the NC
238            replica is present (true) or if the IT_NC_GOING flag is
239            set then the NC replica is not present (false)
240         """
241         if self.rep_present_criteria_one and \
242            self.rep_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0:
243             return True
244         return False
245
246
247 class DirectoryServiceAgent:
248
249     def __init__(self, dsa_dnstr):
250         """Initialize DSA class.  Class is subsequently
251            fully populated by calling the load_dsa() method
252            :param dsa_dnstr:  DN of the nTDSDSA
253         """
254         self.dsa_dnstr     = dsa_dnstr
255         self.dsa_guid      = None
256         self.dsa_ivid      = None
257         self.dsa_is_ro     = False
258         self.dsa_is_gc     = False
259         self.dsa_behavior  = 0
260         self.default_dnstr = None  # default domain dn string for dsa
261
262         # NCReplicas for this dsa.
263         # Indexed by DN string of naming context
264         self.rep_table     = {}
265
266         # NTDSConnections for this dsa.
267         # Indexed by DN string of connection
268         self.connect_table = {}
269         return
270
271     def __str__(self):
272         '''Debug dump string output of class'''
273         text = ""
274         if self.dsa_dnstr:
275             text = text + "\n\tdn=%s"   % self.dsa_dnstr
276         if self.dsa_guid:
277             text = text + "\n\tguid=%s" % str(self.dsa_guid)
278         if self.dsa_ivid:
279             text = text + "\n\tivid=%s" % str(self.dsa_ivid)
280
281         text = text + "\n\tro=%s:gc=%s" % (self.dsa_is_ro, self.dsa_is_gc)
282         return "%s:%s\n%s\n%s" % (self.__class__.__name__, text,
283                                   self.dumpstr_replica_table(),
284                                   self.dumpstr_connect_table())
285
286     def is_ro(self):
287         '''Returns True if dsa a read only domain controller'''
288         return self.dsa_is_ro
289
290     def is_gc(self):
291         '''Returns True if dsa hosts a global catalog'''
292         return self.dsa_is_gc
293
294     def is_minimum_behavior(self, version):
295         """Is dsa at minimum windows level greater than or
296            equal to (version)
297            :param version: Windows version to test against
298                           (e.g. DS_BEHAVIOR_WIN2008)
299         """
300         if self.dsa_behavior >= version:
301             return True
302         return False
303
304     def load_dsa(self, samdb):
305         """Method to load a DSA from the samdb.  Prior initialization
306            has given us the DN of the DSA that we are to load.  This
307            method initializes all other attributes, including loading
308            the NC replica table for this DSA.
309            Raises an Exception on error.
310         """
311         controls = [ "extended_dn:1:1" ]
312         attrs    = [ "objectGUID",
313                      "invocationID",
314                      "options",
315                      "msDS-isRODC",
316                      "msDS-Behavior-Version" ]
317         try:
318             res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
319                                attrs=attrs, controls=controls)
320
321         except ldb.LdbError, (enum, estr):
322             raise Exception("Unable to find nTDSDSA for (%s) - (%s)" % \
323                             (self.dsa_dnstr, estr))
324             return
325
326         msg = res[0]
327         self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID",
328                                   msg["objectGUID"][0]))
329
330         # RODCs don't originate changes and thus have no invocationId,
331         # therefore we must check for existence first
332         if "invocationId" in msg:
333             self.dsa_ivid = misc.GUID(samdb.schema_format_value("objectGUID",
334                                       msg["invocationId"][0]))
335
336         if "options" in msg and \
337             ((int(msg["options"][0]) & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0):
338             self.dsa_is_gc = True
339         else:
340             self.dsa_is_gc = False
341
342         if "msDS-isRODC" in msg and msg["msDS-isRODC"][0] == "TRUE":
343             self.dsa_is_ro = True
344         else:
345             self.dsa_is_ro = False
346
347         if "msDS-Behavior-Version" in msg:
348             self.dsa_behavior = int(msg['msDS-Behavior-Version'][0])
349
350         # Load the NC replicas that are enumerated on this dsa
351         self.load_replica_table(samdb)
352
353         # Load the nTDSConnection that are enumerated on this dsa
354         self.load_connection_table(samdb)
355
356         return
357
358
359     def load_replica_table(self, samdb):
360         """Method to load the NC replica's listed for DSA object. This
361            method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs,
362            hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs,
363            and msDS-HasInstantiatedNCs) to determine complete list of
364            NC replicas that are enumerated for the DSA.  Once a NC
365            replica is loaded it is identified (schema, config, etc) and
366            the other replica attributes (partial, ro, etc) are determined.
367            Raises an Exception on error.
368            :param samdb: database to query for DSA replica list
369         """
370         controls = ["extended_dn:1:1"]
371         ncattrs = [ # not RODC - default, config, schema (old style)
372                     "hasMasterNCs",
373                     # not RODC - default, config, schema, app NCs
374                     "msDS-hasMasterNCs",
375                     # domain NC partial replicas
376                     "hasPartialReplicANCs",
377                     # default domain NC
378                     "msDS-HasDomainNCs",
379                     # RODC only - default, config, schema, app NCs
380                     "msDS-hasFullReplicaNCs",
381                     # Identifies if replica is coming, going, or stable
382                     "msDS-HasInstantiatedNCs" ]
383         try:
384             res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
385                                attrs=ncattrs, controls=controls)
386
387         except ldb.LdbError, (enum, estr):
388             raise Exception("Unable to find nTDSDSA NCs for (%s) - (%s)" % \
389                             (self.dsa_dnstr, estr))
390             return
391
392         # The table of NCs for the dsa we are searching
393         tmp_table = {}
394
395         # We should get one response to our query here for
396         # the ntds that we requested
397         if len(res[0]) > 0:
398
399             # Our response will contain a number of elements including
400             # the dn of the dsa as well as elements for each
401             # attribute (e.g. hasMasterNCs).  Each of these elements
402             # is a dictonary list which we retrieve the keys for and
403             # then iterate over them
404             for k in res[0].keys():
405                 if k == "dn":
406                     continue
407
408                 # For each attribute type there will be one or more DNs
409                 # listed.  For instance DCs normally have 3 hasMasterNCs
410                 # listed.
411                 for value in res[0][k]:
412                     # Turn dn into a dsdb_Dn so we can use
413                     # its methods to parse the extended pieces.
414                     # Note we don't really need the exact sid value
415                     # but instead only need to know if its present.
416                     dsdn  = dsdb_Dn(samdb, value)
417                     guid  = dsdn.dn.get_extended_component('GUID')
418                     sid   = dsdn.dn.get_extended_component('SID')
419                     flags = dsdn.get_binary_integer()
420                     dnstr = str(dsdn.dn)
421
422                     if guid is None:
423                         raise Exception("Missing GUID for (%s) - (%s: %s)" % \
424                                         (self.dsa_dnstr, k, value))
425                     else:
426                         guidstr = str(misc.GUID(guid))
427
428                     if not dnstr in tmp_table:
429                         rep = NCReplica(self.dsa_dnstr, self.dsa_guid,
430                                         dnstr, guidstr, sid)
431                         tmp_table[dnstr] = rep
432                     else:
433                         rep = tmp_table[dnstr]
434
435                     if k == "msDS-HasInstantiatedNCs":
436                         rep.set_replica_flags(flags)
437                         continue
438
439                     rep.identify_by_dsa_attr(samdb, k)
440
441                     # if we've identified the default domain NC
442                     # then save its DN string
443                     if rep.is_default():
444                        self.default_dnstr = dnstr
445         else:
446             raise Exception("No nTDSDSA NCs for (%s)" % self.dsa_dnstr)
447             return
448
449         # Assign our newly built NC replica table to this dsa
450         self.rep_table = tmp_table
451         return
452
453     def load_connection_table(self, samdb):
454         """Method to load the nTDSConnections listed for DSA object.
455            Raises an Exception on error.
456            :param samdb: database to query for DSA connection list
457         """
458         try:
459             res = samdb.search(base=self.dsa_dnstr,
460                                scope=ldb.SCOPE_SUBTREE,
461                                expression="(objectClass=nTDSConnection)")
462
463         except ldb.LdbError, (enum, estr):
464             raise Exception("Unable to find nTDSConnection for (%s) - (%s)" % \
465                             (self.dsa_dnstr, estr))
466             return
467
468         for msg in res:
469             dnstr = str(msg.dn)
470
471             # already loaded
472             if dnstr in self.connect_table.keys():
473                 continue
474
475             connect = NTDSConnection(dnstr)
476
477             connect.load_connection(samdb)
478             self.connect_table[dnstr] = connect
479         return
480
481     def commit_connection_table(self, samdb):
482         """Method to commit any uncommitted nTDSConnections
483            that are in our table.  These would be newly identified
484            connections that are marked as (committed = False)
485            :param samdb: database to commit DSA connection list to
486         """
487         for dnstr, connect in self.connect_table.items():
488             connect.commit_connection(samdb)
489
490     def add_connection_by_dnstr(self, dnstr, connect):
491         self.connect_table[dnstr] = connect
492         return
493
494     def get_connection_by_from_dnstr(self, from_dnstr):
495         """Scan DSA nTDSConnection table and return connection
496            with a "fromServer" dn string equivalent to method
497            input parameter.
498            :param from_dnstr: search for this from server entry
499         """
500         for dnstr, connect in self.connect_table.items():
501             if connect.get_from_dnstr() == from_dnstr:
502                 return connect
503         return None
504
505     def dumpstr_replica_table(self):
506         '''Debug dump string output of replica table'''
507         text=""
508         for k in self.rep_table.keys():
509             if text:
510                 text = text + "\n%s" % self.rep_table[k]
511             else:
512                 text = "%s" % self.rep_table[k]
513         return text
514
515     def dumpstr_connect_table(self):
516         '''Debug dump string output of connect table'''
517         text=""
518         for k in self.connect_table.keys():
519             if text:
520                 text = text + "\n%s" % self.connect_table[k]
521             else:
522                 text = "%s" % self.connect_table[k]
523         return text
524
525 class NTDSConnection():
526     """Class defines a nTDSConnection found under a DSA
527     """
528     def __init__(self, dnstr):
529         self.dnstr       = dnstr
530         self.enabled     = False
531         self.committed   = False # appears in database
532         self.options     = 0
533         self.flags       = 0
534         self.from_dnstr  = None
535         self.schedulestr = None
536         return
537
538     def __str__(self):
539         '''Debug dump string output of NTDSConnection object'''
540         text = "%s: %s" % (self.__class__.__name__, self.dnstr)
541         text = text + "\n\tenabled: %s" % self.enabled
542         text = text + "\n\tcommitted: %s" % self.committed
543         text = text + "\n\toptions: 0x%08X" % self.options
544         text = text + "\n\tflags: 0x%08X" % self.flags
545         text = text + "\n\tfrom_dn: %s" % self.from_dnstr
546         return text
547
548     def load_connection(self, samdb):
549         """Given a NTDSConnection object with an prior initialization
550            for the object's DN, search for the DN and load attributes
551            from the samdb.
552            Raises an Exception on error.
553         """
554         attrs = [ "options",
555                   "enabledConnection",
556                   "schedule",
557                   "fromServer",
558                   "systemFlags" ]
559         try:
560             res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
561                                attrs=attrs)
562
563         except ldb.LdbError, (enum, estr):
564             raise Exception("Unable to find nTDSConnection for (%s) - (%s)" % \
565                             (self.dnstr, estr))
566             return
567
568         msg = res[0]
569
570         if "options" in msg:
571             self.options = int(msg["options"][0])
572         if "enabledConnection" in msg:
573             if msg["enabledConnection"][0].upper().lstrip().rstrip() == "TRUE":
574                 self.enabled = True
575         if "systemFlags" in msg:
576             self.flags = int(msg["systemFlags"][0])
577         if "schedule" in msg:
578             self.schedulestr = msg["schedule"][0]
579         if "fromServer" in msg:
580             dsdn = dsdb_Dn(samdb, msg["fromServer"][0])
581             self.from_dnstr = str(dsdn.dn)
582             assert self.from_dnstr != None
583
584         # Appears as committed in the database
585         self.committed = True
586         return
587
588     def commit_connection(self, samdb):
589         """Given a NTDSConnection object that is not committed in the
590            sam database, perform a commit action.
591         """
592         if self.committed: # nothing to do
593             return
594
595         # XXX - not yet written
596         return
597
598     def get_from_dnstr(self):
599         '''Return fromServer dn string attribute'''
600         return self.from_dnstr
601
602 class Partition(NamingContext):
603     """Class defines a naming context discovered thru the
604        Partitions DN of the configuration schema.  This is
605        a more specific form of NamingContext class (inheriting
606        from that class) and it identifies unique attributes
607        enumerated in the Partitions such as which nTDSDSAs
608        are cross referenced for replicas
609     """
610     def __init__(self, partstr):
611         self.partstr          = partstr
612         self.rw_location_list = []
613         self.ro_location_list = []
614
615         # We don't have enough info to properly
616         # fill in the naming context yet.  We'll get that
617         # fully set up with load_partition().
618         NamingContext.__init__(self, None)
619
620
621     def load_partition(self, samdb):
622         """Given a Partition class object that has been initialized
623            with its partition dn string, load the partition from the
624            sam database, identify the type of the partition (schema,
625            domain, etc) and record the list of nTDSDSAs that appear
626            in the cross reference attributes msDS-NC-Replica-Locations
627            and msDS-NC-RO-Replica-Locations.
628            Raises an Exception on error.
629            :param samdb: sam database to load partition from
630         """
631         controls = ["extended_dn:1:1"]
632         attrs = [ "nCName",
633                   "msDS-NC-Replica-Locations",
634                   "msDS-NC-RO-Replica-Locations" ]
635         try:
636             res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE,
637                                attrs=attrs, controls=controls)
638
639         except ldb.LdbError, (enum, estr):
640             raise Exception("Unable to find partition for (%s) - (%s)" % (
641                             self.partstr, estr))
642             return
643
644         msg = res[0]
645         for k in msg.keys():
646             if k == "dn":
647                 continue
648
649             for value in msg[k]:
650                 # Turn dn into a dsdb_Dn so we can use
651                 # its methods to parse the extended pieces.
652                 # Note we don't really need the exact sid value
653                 # but instead only need to know if its present.
654                 dsdn  = dsdb_Dn(samdb, value)
655                 guid  = dsdn.dn.get_extended_component('GUID')
656                 sid   = dsdn.dn.get_extended_component('SID')
657
658                 if guid is None:
659                     raise Exception("Missing GUID for (%s) - (%s: %s)" % \
660                                     (self.partstr, k, value))
661                 else:
662                     guidstr = str(misc.GUID(guid))
663
664                 if k == "nCName":
665                     self.nc_dnstr = str(dsdn.dn)
666                     self.nc_guid  = guidstr
667                     self.nc_sid   = sid
668                     continue
669
670                 if k == "msDS-NC-Replica-Locations":
671                     self.rw_location_list.append(str(dsdn.dn))
672                     continue
673
674                 if k == "msDS-NC-RO-Replica-Locations":
675                     self.ro_location_list.append(str(dsdn.dn))
676                     continue
677
678         # Now identify what type of NC this partition
679         # enumerated
680         self.identify_by_basedn(samdb)
681
682         return
683
684     def should_be_present(self, target_dsa):
685         """Tests whether this partition should have an NC replica
686            on the target dsa.  This method returns a tuple of
687            needed=True/False, ro=True/False, partial=True/False
688            :param target_dsa: should NC be present on target dsa
689         """
690         needed  = False
691         ro      = False
692         partial = False
693
694         # If this is the config, schema, or default
695         # domain NC for the target dsa then it should
696         # be present
697         if self.nc_type == NCType.config or \
698            self.nc_type == NCType.schema or \
699            (self.nc_type == NCType.domain and \
700             self.nc_dnstr == target_dsa.default_dnstr):
701             needed = True
702
703         # A writable replica of an application NC should be present
704         # if there a cross reference to the target DSA exists.  Depending
705         # on whether the DSA is ro we examine which type of cross reference
706         # to look for (msDS-NC-Replica-Locations or
707         # msDS-NC-RO-Replica-Locations
708         if self.nc_type == NCType.application:
709             if target_dsa.is_ro():
710                if target_dsa.dsa_dnstr in self.ro_location_list:
711                    needed = True
712             else:
713                if target_dsa.dsa_dnstr in self.rw_location_list:
714                    needed = True
715
716         # If the target dsa is a gc then a partial replica of a
717         # domain NC (other than the DSAs default domain) should exist
718         # if there is also a cross reference for the DSA
719         if target_dsa.is_gc() and \
720            self.nc_type == NCType.domain and \
721            self.nc_dnstr != target_dsa.default_dnstr and \
722            (target_dsa.dsa_dnstr in self.ro_location_list or \
723             target_dsa.dsa_dnstr in self.rw_location_list):
724             needed  = True
725             partial = True
726
727         # partial NCs are always readonly
728         if needed and (target_dsa.is_ro() or partial):
729             ro = True
730
731         return needed, ro, partial
732
733     def __str__(self):
734         '''Debug dump string output of class'''
735         text = "%s" % NamingContext.__str__(self)
736         text = text + "\n\tpartdn=%s" % self.partstr
737         for k in self.rw_location_list:
738             text = text + "\n\tmsDS-NC-Replica-Locations=%s" % k
739         for k in self.ro_location_list:
740             text = text + "\n\tmsDS-NC-RO-Replica-Locations=%s" % k
741         return text
742
743 class Site:
744     def __init__(self, site_dnstr):
745         self.site_dnstr   = site_dnstr
746         self.site_options = 0
747         return
748
749     def load_site(self, samdb):
750         """Loads the NTDS Site Settions options attribute for the site
751            Raises an Exception on error.
752         """
753         ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
754         try:
755             res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE,
756                                attrs=["options"])
757         except ldb.LdbError, (enum, estr):
758             raise Exception("Unable to find site settings for (%s) - (%s)" % \
759                             (ssdn, estr))
760             return
761
762         msg = res[0]
763         if "options" in msg:
764             self.site_options = int(msg["options"][0])
765         return
766
767     def is_same_site(self, target_dsa):
768         '''Determine if target dsa is in this site'''
769         if self.site_dnstr in target_dsa.dsa_dnstr:
770             return True
771         return False
772
773     def is_intrasite_topology_disabled(self):
774         '''Returns True if intrasite topology is disabled for site'''
775         if (self.site_options & \
776             dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0:
777             return True
778         return False
779
780     def should_detect_stale(self):
781         '''Returns True if detect stale is enabled for site'''
782         if (self.site_options & \
783             dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) == 0:
784             return True
785         return False
786
787
788 class GraphNode:
789     """This is a graph node describing a set of edges that should be
790        directed to it.  Each edge is a connection for a particular
791        naming context replica directed from another node in the forest
792        to this node.
793     """
794     def __init__(self, dsa_dnstr, max_node_edges):
795         """Instantiate the graph node according to a DSA dn string
796            :param max_node_edges: maximum number of edges that should ever
797                                   be directed to the node
798         """
799         self.max_edges = max_node_edges
800         self.dsa_dnstr = dsa_dnstr
801         self.edge_from = []
802
803     def __str__(self):
804         text = "%s: %s" % (self.__class__.__name__, self.dsa_dnstr)
805         for edge in self.edge_from:
806             text = text + "\n\tedge from: %s" % edge
807         return text
808
809     def add_edge_from(self, from_dsa_dnstr):
810         """Add an edge from the dsa to our graph nodes edge from list
811            :param from_dsa_dnstr: the dsa that the edge emanates from
812         """
813         assert from_dsa_dnstr != None
814
815         # No edges from myself to myself
816         if from_dsa_dnstr == self.dsa_dnstr:
817             return False
818         # Only one edge from a particular node
819         if from_dsa_dnstr in self.edge_from:
820             return False
821         # Not too many edges
822         if len(self.edge_from) >= self.max_edges:
823             return False
824         self.edge_from.append(from_dsa_dnstr)
825         return True
826
827     def add_edges_from_connections(self, dsa):
828         """For each nTDSConnection object associated with a particular
829            DSA, we test if it implies an edge to this graph node (i.e.
830            the "fromServer" attribute).  If it does then we add an
831            edge from the server unless we are over the max edges for this
832            graph node
833            :param dsa: dsa with a dnstr equivalent to his graph node
834         """
835         for dnstr, connect in dsa.connect_table.items():
836             self.add_edge_from(connect.from_dnstr)
837         return
838
839     def add_connections_from_edges(self, dsa):
840         """For each edge directed to this graph node, ensure there
841            is a corresponding nTDSConnection object in the dsa.
842         """
843         for edge_dnstr in self.edge_from:
844             connect = dsa.get_connection_by_from_dnstr(edge_dnstr)
845
846             # For each edge directed to the NC replica that
847             # "should be present" on the local DC, the KCC determines
848             # whether an object c exists such that:
849             #
850             #    c is a child of the DC's nTDSDSA object.
851             #    c.objectCategory = nTDSConnection
852             #
853             # Given the NC replica ri from which the edge is directed,
854             #    c.fromServer is the dsname of the nTDSDSA object of
855             #    the DC on which ri "is present".
856             #
857             #    c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
858             if connect and \
859                connect.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
860                 exists = True
861             else:
862                 exists = False
863
864             # if no such object exists then the KCC adds an object
865             # c with the following attributes
866             if exists:
867                 return
868
869             # Generate a new dnstr for this nTDSConnection
870             dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
871
872             connect = NTDSConnection(dnstr)
873             connect.enabled    = True
874             connect.committed  = False
875             connect.from_dnstr = edge_dnstr
876             connect.options    = dsdb.NTDSCONN_OPT_IS_GENERATED
877             connect.flags      = dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME + \
878                                  dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE
879
880             # XXX I need to write the schedule blob
881
882             dsa.add_connection_by_dnstr(dnstr, connect);
883
884         return
885
886     def has_sufficient_edges(self):
887         '''Return True if we have met the maximum "from edges" criteria'''
888         if len(self.edge_from) >= self.max_edges:
889             return True
890         return False