samba-tool: dbcheck avoid problems with deleted objects
[Samba/gebeck_regimport.git] / source4 / scripting / python / samba / kcc_utils.py
blobac7449acd02f22d140677f023aaecab487795269
1 #!/usr/bin/env python
3 # KCC topology utilities
5 # Copyright (C) Dave Craft 2011
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.
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.
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/>.
20 import samba, ldb
21 import uuid
23 from samba import dsdb
24 from samba.dcerpc import misc
25 from samba.common import dsdb_Dn
27 class NCType:
28 (unknown, schema, domain, config, application) = range(0, 5)
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 """
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
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)
54 def is_schema(self):
55 '''Return True if NC is schema'''
56 return self.nc_type == NCType.schema
58 def is_domain(self):
59 '''Return True if NC is domain'''
60 return self.nc_type == NCType.domain
62 def is_application(self):
63 '''Return True if NC is application'''
64 return self.nc_type == NCType.application
66 def is_config(self):
67 '''Return True if NC is config'''
68 return self.nc_type == NCType.config
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
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
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
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)
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)
119 return
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.
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
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
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
151 # Call my super class we inherited from
152 NamingContext.__init__(self, nc_dnstr, nc_guid, nc_sid)
153 return
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)
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
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
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
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
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
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
205 # RODC only
206 elif attr == "msDS-hasFullReplicaNCs":
207 self.rep_present_criteria_one = True
208 self.rep_ro = True
210 # Not RODC
211 elif attr == "msDS-hasMasterNCs":
212 self.rep_ro = False
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
220 def is_default(self):
221 """Returns True if this is a default domain NC for the dsa
222 that this NC appears on
224 return self.rep_default
226 def is_ro(self):
227 '''Return True if NC replica is read only'''
228 return self.rep_ro
230 def is_partial(self):
231 '''Return True if NC replica is partial'''
232 return self.rep_partial
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)
241 if self.rep_present_criteria_one and \
242 self.rep_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0:
243 return True
244 return False
247 class DirectoryServiceAgent:
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
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
262 # NCReplicas for this dsa.
263 # Indexed by DN string of naming context
264 self.rep_table = {}
266 # NTDSConnections for this dsa.
267 # Indexed by DN string of connection
268 self.connect_table = {}
269 return
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)
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())
286 def is_ro(self):
287 '''Returns True if dsa a read only domain controller'''
288 return self.dsa_is_ro
290 def is_gc(self):
291 '''Returns True if dsa hosts a global catalog'''
292 return self.dsa_is_gc
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)
300 if self.dsa_behavior >= version:
301 return True
302 return False
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.
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)
321 except ldb.LdbError, (enum, estr):
322 raise Exception("Unable to find nTDSDSA for (%s) - (%s)" % \
323 (self.dsa_dnstr, estr))
324 return
326 msg = res[0]
327 self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID",
328 msg["objectGUID"][0]))
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]))
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
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
347 if "msDS-Behavior-Version" in msg:
348 self.dsa_behavior = int(msg['msDS-Behavior-Version'][0])
350 # Load the NC replicas that are enumerated on this dsa
351 self.load_replica_table(samdb)
353 # Load the nTDSConnection that are enumerated on this dsa
354 self.load_connection_table(samdb)
356 return
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
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)
387 except ldb.LdbError, (enum, estr):
388 raise Exception("Unable to find nTDSDSA NCs for (%s) - (%s)" % \
389 (self.dsa_dnstr, estr))
390 return
392 # The table of NCs for the dsa we are searching
393 tmp_table = {}
395 # We should get one response to our query here for
396 # the ntds that we requested
397 if len(res[0]) > 0:
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
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)
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))
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]
435 if k == "msDS-HasInstantiatedNCs":
436 rep.set_replica_flags(flags)
437 continue
439 rep.identify_by_dsa_attr(samdb, k)
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
449 # Assign our newly built NC replica table to this dsa
450 self.rep_table = tmp_table
451 return
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
458 try:
459 res = samdb.search(base=self.dsa_dnstr,
460 scope=ldb.SCOPE_SUBTREE,
461 expression="(objectClass=nTDSConnection)")
463 except ldb.LdbError, (enum, estr):
464 raise Exception("Unable to find nTDSConnection for (%s) - (%s)" % \
465 (self.dsa_dnstr, estr))
466 return
468 for msg in res:
469 dnstr = str(msg.dn)
471 # already loaded
472 if dnstr in self.connect_table.keys():
473 continue
475 connect = NTDSConnection(dnstr)
477 connect.load_connection(samdb)
478 self.connect_table[dnstr] = connect
479 return
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
487 for dnstr, connect in self.connect_table.items():
488 connect.commit_connection(samdb)
490 def add_connection_by_dnstr(self, dnstr, connect):
491 self.connect_table[dnstr] = connect
492 return
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
500 for dnstr, connect in self.connect_table.items():
501 if connect.get_from_dnstr() == from_dnstr:
502 return connect
503 return None
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
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
525 class NTDSConnection():
526 """Class defines a nTDSConnection found under a DSA
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
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
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.
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)
563 except ldb.LdbError, (enum, estr):
564 raise Exception("Unable to find nTDSConnection for (%s) - (%s)" % \
565 (self.dnstr, estr))
566 return
568 msg = res[0]
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
584 # Appears as committed in the database
585 self.committed = True
586 return
588 def commit_connection(self, samdb):
589 """Given a NTDSConnection object that is not committed in the
590 sam database, perform a commit action.
592 if self.committed: # nothing to do
593 return
595 # XXX - not yet written
596 return
598 def get_from_dnstr(self):
599 '''Return fromServer dn string attribute'''
600 return self.from_dnstr
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
610 def __init__(self, partstr):
611 self.partstr = partstr
612 self.rw_location_list = []
613 self.ro_location_list = []
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)
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
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)
639 except ldb.LdbError, (enum, estr):
640 raise Exception("Unable to find partition for (%s) - (%s)" % (
641 self.partstr, estr))
642 return
644 msg = res[0]
645 for k in msg.keys():
646 if k == "dn":
647 continue
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')
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))
664 if k == "nCName":
665 self.nc_dnstr = str(dsdn.dn)
666 self.nc_guid = guidstr
667 self.nc_sid = sid
668 continue
670 if k == "msDS-NC-Replica-Locations":
671 self.rw_location_list.append(str(dsdn.dn))
672 continue
674 if k == "msDS-NC-RO-Replica-Locations":
675 self.ro_location_list.append(str(dsdn.dn))
676 continue
678 # Now identify what type of NC this partition
679 # enumerated
680 self.identify_by_basedn(samdb)
682 return
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
690 needed = False
691 ro = False
692 partial = False
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
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
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
727 # partial NCs are always readonly
728 if needed and (target_dsa.is_ro() or partial):
729 ro = True
731 return needed, ro, partial
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
743 class Site:
744 def __init__(self, site_dnstr):
745 self.site_dnstr = site_dnstr
746 self.site_options = 0
747 return
749 def load_site(self, samdb):
750 """Loads the NTDS Site Settions options attribute for the site
751 Raises an Exception on error.
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
762 msg = res[0]
763 if "options" in msg:
764 self.site_options = int(msg["options"][0])
765 return
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
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
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
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.
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
799 self.max_edges = max_node_edges
800 self.dsa_dnstr = dsa_dnstr
801 self.edge_from = []
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
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
813 assert from_dsa_dnstr != None
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
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
835 for dnstr, connect in dsa.connect_table.items():
836 self.add_edge_from(connect.from_dnstr)
837 return
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.
843 for edge_dnstr in self.edge_from:
844 connect = dsa.get_connection_by_from_dnstr(edge_dnstr)
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:
850 # c is a child of the DC's nTDSDSA object.
851 # c.objectCategory = nTDSConnection
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".
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
864 # if no such object exists then the KCC adds an object
865 # c with the following attributes
866 if exists:
867 return
869 # Generate a new dnstr for this nTDSConnection
870 dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
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
880 # XXX I need to write the schedule blob
882 dsa.add_connection_by_dnstr(dnstr, connect);
884 return
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