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/>.
23 from samba
import dsdb
24 from samba
.dcerpc
import misc
25 from samba
.common
import dsdb_Dn
28 (unknown
, schema
, domain
, config
, application
) = range(0, 5)
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
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
42 self
.nc_dnstr
= nc_dnstr
43 self
.nc_guid
= nc_guid
45 self
.nc_type
= NCType
.unknown
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
)
55 '''Return True if NC is schema'''
56 return self
.nc_type
== NCType
.schema
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
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
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
85 self
.nc_type
= NCType
.application
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
94 :param attr: attr of nTDSDSA object where NC DN appears
96 # If the NC is listed under msDS-HasDomainNCs then
97 # this can only be a domain NC and it is our default
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
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
)
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
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
)
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'''
169 self
.rep_flags
= flags
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
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
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
206 elif attr
== "msDS-hasFullReplicaNCs":
207 self
.rep_present_criteria_one
= True
211 elif attr
== "msDS-hasMasterNCs":
214 # Now use this DSA attribute to identify the naming
215 # context type by calling the super class method
217 NamingContext
.identify_by_dsa_attr(self
, samdb
, attr
)
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
227 '''Return True if NC replica is read only'''
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:
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
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
266 # NTDSConnections for this dsa.
267 # Indexed by DN string of connection
268 self
.connect_table
= {}
272 '''Debug dump string output of class'''
275 text
= text
+ "\n\tdn=%s" % self
.dsa_dnstr
277 text
= text
+ "\n\tguid=%s" % str(self
.dsa_guid
)
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())
287 '''Returns True if dsa a read only domain controller'''
288 return self
.dsa_is_ro
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
297 :param version: Windows version to test against
298 (e.g. DS_BEHAVIOR_WIN2008)
300 if self
.dsa_behavior
>= version
:
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",
316 "msDS-Behavior-Version" ]
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
))
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
340 self
.dsa_is_gc
= False
342 if "msDS-isRODC" in msg
and msg
["msDS-isRODC"][0] == "TRUE":
343 self
.dsa_is_ro
= True
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
)
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)
373 # not RODC - default, config, schema, app NCs
375 # domain NC partial replicas
376 "hasPartialReplicANCs",
379 # RODC only - default, config, schema, app NCs
380 "msDS-hasFullReplicaNCs",
381 # Identifies if replica is coming, going, or stable
382 "msDS-HasInstantiatedNCs" ]
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
))
392 # The table of NCs for the dsa we are searching
395 # We should get one response to our query here for
396 # the ntds that we requested
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():
408 # For each attribute type there will be one or more DNs
409 # listed. For instance DCs normally have 3 hasMasterNCs
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()
423 raise Exception("Missing GUID for (%s) - (%s: %s)" % \
424 (self
.dsa_dnstr
, k
, value
))
426 guidstr
= str(misc
.GUID(guid
))
428 if not dnstr
in tmp_table
:
429 rep
= NCReplica(self
.dsa_dnstr
, self
.dsa_guid
,
431 tmp_table
[dnstr
] = rep
433 rep
= tmp_table
[dnstr
]
435 if k
== "msDS-HasInstantiatedNCs":
436 rep
.set_replica_flags(flags
)
439 rep
.identify_by_dsa_attr(samdb
, k
)
441 # if we've identified the default domain NC
442 # then save its DN string
444 self
.default_dnstr
= dnstr
446 raise Exception("No nTDSDSA NCs for (%s)" % self
.dsa_dnstr
)
449 # Assign our newly built NC replica table to this dsa
450 self
.rep_table
= tmp_table
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
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
))
472 if dnstr
in self
.connect_table
.keys():
475 connect
= NTDSConnection(dnstr
)
477 connect
.load_connection(samdb
)
478 self
.connect_table
[dnstr
] = connect
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
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
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
:
505 def dumpstr_replica_table(self
):
506 '''Debug dump string output of replica table'''
508 for k
in self
.rep_table
.keys():
510 text
= text
+ "\n%s" % self
.rep_table
[k
]
512 text
= "%s" % self
.rep_table
[k
]
515 def dumpstr_connect_table(self
):
516 '''Debug dump string output of connect table'''
518 for k
in self
.connect_table
.keys():
520 text
= text
+ "\n%s" % self
.connect_table
[k
]
522 text
= "%s" % self
.connect_table
[k
]
525 class NTDSConnection():
526 """Class defines a nTDSConnection found under a DSA
528 def __init__(self
, dnstr
):
531 self
.committed
= False # appears in database
534 self
.from_dnstr
= None
535 self
.schedulestr
= None
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
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
552 Raises an Exception on error.
560 res
= samdb
.search(base
=self
.dnstr
, scope
=ldb
.SCOPE_BASE
,
563 except ldb
.LdbError
, (enum
, estr
):
564 raise Exception("Unable to find nTDSConnection for (%s) - (%s)" % \
571 self
.options
= int(msg
["options"][0])
572 if "enabledConnection" in msg
:
573 if msg
["enabledConnection"][0].upper().lstrip().rstrip() == "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
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
595 # XXX - not yet written
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"]
633 "msDS-NC-Replica-Locations",
634 "msDS-NC-RO-Replica-Locations" ]
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)" % (
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')
659 raise Exception("Missing GUID for (%s) - (%s: %s)" % \
660 (self
.partstr
, k
, value
))
662 guidstr
= str(misc
.GUID(guid
))
665 self
.nc_dnstr
= str(dsdn
.dn
)
666 self
.nc_guid
= guidstr
670 if k
== "msDS-NC-Replica-Locations":
671 self
.rw_location_list
.append(str(dsdn
.dn
))
674 if k
== "msDS-NC-RO-Replica-Locations":
675 self
.ro_location_list
.append(str(dsdn
.dn
))
678 # Now identify what type of NC this partition
680 self
.identify_by_basedn(samdb
)
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
694 # If this is the config, schema, or default
695 # domain NC for the target dsa then it should
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
):
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
:
713 if target_dsa
.dsa_dnstr
in self
.rw_location_list
:
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
):
727 # partial NCs are always readonly
728 if needed
and (target_dsa
.is_ro() or partial
):
731 return needed
, ro
, partial
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
744 def __init__(self
, site_dnstr
):
745 self
.site_dnstr
= site_dnstr
746 self
.site_options
= 0
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
755 res
= samdb
.search(base
=ssdn
, scope
=ldb
.SCOPE_BASE
,
757 except ldb
.LdbError
, (enum
, estr
):
758 raise Exception("Unable to find site settings for (%s) - (%s)" % \
764 self
.site_options
= int(msg
["options"][0])
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
:
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:
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:
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
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
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
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
:
818 # Only one edge from a particular node
819 if from_dsa_dnstr
in self
.edge_from
:
822 if len(self
.edge_from
) >= self
.max_edges
:
824 self
.edge_from
.append(from_dsa_dnstr
)
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
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
)
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
859 connect
.options
& dsdb
.NTDSCONN_OPT_RODC_TOPOLOGY
== 0:
864 # if no such object exists then the KCC adds an object
865 # c with the following attributes
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
);
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
: