diff options
| -rw-r--r-- | source4/scripting/python/samba/kcc_utils.py | 890 | 
1 files changed, 890 insertions, 0 deletions
diff --git a/source4/scripting/python/samba/kcc_utils.py b/source4/scripting/python/samba/kcc_utils.py new file mode 100644 index 0000000000..a833d30e6c --- /dev/null +++ b/source4/scripting/python/samba/kcc_utils.py @@ -0,0 +1,890 @@ +#!/usr/bin/env python +# +# KCC topology utilities +# +# Copyright (C) Dave Craft 2011 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import samba, ldb +import uuid + +from samba              import dsdb +from samba.dcerpc       import misc +from samba.common       import dsdb_Dn + +class NCType: +    (unknown, schema, domain, config, application) = range(0, 5) + +class NamingContext: +    """Base class for a naming context.  Holds the DN, +       GUID, SID (if available) and type of the DN. +       Subclasses may inherit from this and specialize +    """ + +    def __init__(self, nc_dnstr, nc_guid=None, nc_sid=None): +        """Instantiate a NamingContext +            :param nc_dnstr: NC dn string +            :param nc_guid: NC guid string +            :param nc_sid: NC sid +        """ +	self.nc_dnstr = nc_dnstr +	self.nc_guid  = nc_guid +	self.nc_sid   = nc_sid +	self.nc_type  = NCType.unknown +	return + +    def __str__(self): +        '''Debug dump string output of class''' +        return "%s:\n\tdn=%s\n\tguid=%s\n\ttype=%s" % \ +               (self.__class__.__name__, self.nc_dnstr, +                self.nc_guid, self.nc_type) + +    def is_schema(self): +        '''Return True if NC is schema''' +        return self.nc_type == NCType.schema + +    def is_domain(self): +        '''Return True if NC is domain''' +        return self.nc_type == NCType.domain + +    def is_application(self): +        '''Return True if NC is application''' +        return self.nc_type == NCType.application + +    def is_config(self): +        '''Return True if NC is config''' +        return self.nc_type == NCType.config + +    def identify_by_basedn(self, samdb): +        """Given an NC object, identify what type is is thru +           the samdb basedn strings and NC sid value +        """ +        # We check against schema and config because they +        # will be the same for all nTDSDSAs in the forest. +        # That leaves the domain NCs which can be identified +        # by sid and application NCs as the last identified +        if self.nc_dnstr == str(samdb.get_schema_basedn()): +            self.nc_type = NCType.schema +        elif self.nc_dnstr == str(samdb.get_config_basedn()): +            self.nc_type = NCType.config +        elif self.nc_sid != None: +            self.nc_type = NCType.domain +        else: +            self.nc_type = NCType.application +        return + +    def identify_by_dsa_attr(self, samdb, attr): +        """Given an NC which has been discovered thru the +           nTDSDSA database object, determine what type of NC +           it is (i.e. schema, config, domain, application) via +           the use of the schema attribute under which the NC +           was found. +            :param attr: attr of nTDSDSA object where NC DN appears +        """ +        # If the NC is listed under msDS-HasDomainNCs then +        # this can only be a domain NC and it is our default +        # domain for this dsa +        if attr == "msDS-HasDomainNCs": +            self.nc_type = NCType.domain + +        # If the NC is listed under hasPartialReplicaNCs +        # this is only a domain NC +        elif attr == "hasPartialReplicaNCs": +            self.nc_type = NCType.domain + +        # NCs listed under hasMasterNCs are either +        # default domain, schema, or config.  We +        # utilize the identify_by_samdb_basedn() to +        # identify those +        elif attr == "hasMasterNCs": +            self.identify_by_basedn(samdb) + +        # Still unknown (unlikely) but for completeness +        # and for finally identifying application NCs +        if self.nc_type == NCType.unknown: +            self.identify_by_basedn(samdb) + +        return + + +class NCReplica(NamingContext): +    """Class defines a naming context replica that is relative +       to a specific DSA.  This is a more specific form of +       NamingContext class (inheriting from that class) and it +       identifies unique attributes of the DSA's replica for a NC. +    """ + +    def __init__(self, dsa_dnstr, dsa_guid, nc_dnstr, \ +                 nc_guid=None, nc_sid=None): +        """Instantiate a Naming Context Replica +            :param dsa_guid: GUID of DSA where replica appears +            :param nc_dnstr: NC dn string +            :param nc_guid: NC guid string +            :param nc_sid: NC sid +        """ +        self.rep_dsa_dnstr = dsa_dnstr +        self.rep_dsa_guid  = dsa_guid # GUID of DSA where this appears +        self.rep_default   = False # replica for DSA's default domain +        self.rep_partial   = False +        self.rep_ro        = False +        self.rep_flags     = 0 + +        # The (is present) test is a combination of being +        # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or +        # hasPartialReplicaNCs) as well as its replica flags found +        # thru the msDS-HasInstantiatedNCs.  If the NC replica meets +        # the first enumeration test then this flag is set true +        self.rep_present_criteria_one = False + +        # Call my super class we inherited from +        NamingContext.__init__(self, nc_dnstr, nc_guid, nc_sid) +        return + +    def __str__(self): +        '''Debug dump string output of class''' +        text = "default=%s"  % self.rep_default + \ +               ":ro=%s"      % self.rep_ro      + \ +               ":partial=%s" % self.rep_partial + \ +               ":present=%s" % self.is_present() +        return "%s\n\tdsaguid=%s\n\t%s" % \ +               (NamingContext.__str__(self), self.rep_dsa_guid, text) + +    def set_replica_flags(self, flags=None): +        '''Set or clear NC replica flags''' +        if (flags == None): +            self.rep_flags = 0 +        else: +            self.rep_flags = flags +        return + +    def identify_by_dsa_attr(self, samdb, attr): +        """Given an NC which has been discovered thru the +           nTDSDSA database object, determine what type of NC +           replica it is (i.e. partial, read only, default) +            :param attr: attr of nTDSDSA object where NC DN appears +        """ +        # If the NC was found under hasPartialReplicaNCs +        # then a partial replica at this dsa +        if attr == "hasPartialReplicaNCs": +            self.rep_partial = True +            self.rep_present_criteria_one = True + +        # If the NC is listed under msDS-HasDomainNCs then +        # this can only be a domain NC and it is the DSA's +        # default domain NC +        elif attr == "msDS-HasDomainNCs": +            self.rep_default = True + +        # NCs listed under hasMasterNCs are either +        # default domain, schema, or config.  We check +        # against schema and config because they will be +        # the same for all nTDSDSAs in the forest.  That +        # leaves the default domain NC remaining which +        # may be different for each nTDSDSAs (and thus +        # we don't compare agains this samdb's default +        # basedn +        elif attr == "hasMasterNCs": +            self.rep_present_criteria_one = True + +            if self.nc_dnstr != str(samdb.get_schema_basedn()) and \ +               self.nc_dnstr != str(samdb.get_config_basedn()): +                self.rep_default = True + +        # RODC only +        elif attr == "msDS-hasFullReplicaNCs": +            self.rep_present_criteria_one = True +            self.rep_ro = True + +        # Not RODC +        elif attr == "msDS-hasMasterNCs": +            self.rep_ro = False + +        # Now use this DSA attribute to identify the naming +        # context type by calling the super class method +        # of the same name +        NamingContext.identify_by_dsa_attr(self, samdb, attr) +        return + +    def is_default(self): +        """Returns True if this is a default domain NC for the dsa +           that this NC appears on +        """ +        return self.rep_default + +    def is_ro(self): +        '''Return True if NC replica is read only''' +        return self.rep_ro + +    def is_partial(self): +        '''Return True if NC replica is partial''' +        return self.rep_partial + +    def is_present(self): +        """Given an NC replica which has been discovered thru the +           nTDSDSA database object and populated with replica flags +           from the msDS-HasInstantiatedNCs; return whether the NC +           replica is present (true) or if the IT_NC_GOING flag is +           set then the NC replica is not present (false) +        """ +        if self.rep_present_criteria_one and \ +	   self.rep_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0: +            return True +        return False + + +class DirectoryServiceAgent: + +    def __init__(self, dsa_dnstr): +        """Initialize DSA class.  Class is subsequently +           fully populated by calling the load_dsa() method +           :param dsa_dnstr:  DN of the nTDSDSA +        """ +        self.dsa_dnstr     = dsa_dnstr +        self.dsa_guid      = None +        self.dsa_ivid      = None +        self.dsa_is_ro     = False +        self.dsa_is_gc     = False +        self.dsa_behavior  = 0 +        self.default_dnstr = None  # default domain dn string for dsa + +        # NCReplicas for this dsa. +        # Indexed by DN string of naming context +        self.rep_table     = {} + +        # NTDSConnections for this dsa. +        # Indexed by DN string of connection +        self.connect_table = {} +        return + +    def __str__(self): +        '''Debug dump string output of class''' +        text = "" +        if self.dsa_dnstr: +            text = text + "\n\tdn=%s"   % self.dsa_dnstr +        if self.dsa_guid: +            text = text + "\n\tguid=%s" % str(self.dsa_guid) +        if self.dsa_ivid: +            text = text + "\n\tivid=%s" % str(self.dsa_ivid) + +        text = text + "\n\tro=%s:gc=%s" % (self.dsa_is_ro, self.dsa_is_gc) +        return "%s:%s\n%s\n%s" % (self.__class__.__name__, text, +                                  self.dumpstr_replica_table(), +                                  self.dumpstr_connect_table()) + +    def is_ro(self): +        '''Returns True if dsa a read only domain controller''' +        return self.dsa_is_ro + +    def is_gc(self): +        '''Returns True if dsa hosts a global catalog''' +        return self.dsa_is_gc + +    def is_minimum_behavior(self, version): +        """Is dsa at minimum windows level greater than or +           equal to (version) +           :param version: Windows version to test against +                          (e.g. DS_BEHAVIOR_WIN2008) +        """ +        if self.dsa_behavior >= version: +            return True +        return False + +    def load_dsa(self, samdb): +        """Method to load a DSA from the samdb.  Prior initialization +           has given us the DN of the DSA that we are to load.  This +           method initializes all other attributes, including loading +           the NC replica table for this DSA. +           Raises an Exception on error. +        """ +        controls = [ "extended_dn:1:1" ] +	attrs    = [ "objectGUID", +                     "invocationID", +                     "options", +                     "msDS-isRODC", +                     "msDS-Behavior-Version" ] +        try: +            res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE, +                               attrs=attrs, controls=controls) + +        except ldb.LdbError, (enum, estr): +            raise Exception("Unable to find nTDSDSA for (%s) - (%s)" % \ +                            (self.dsa_dnstr, estr)) +            return + +        msg = res[0] +        self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID", +                                  msg["objectGUID"][0])) + +        # RODCs don't originate changes and thus have no invocationId, +        # therefore we must check for existence first +        if "invocationId" in msg: +            self.dsa_ivid = misc.GUID(samdb.schema_format_value("objectGUID", +                                      msg["invocationId"][0])) + +	if "options" in msg and \ +            ((int(msg["options"][0]) & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0): +            self.dsa_is_gc = True +        else: +            self.dsa_is_gc = False + +        if "msDS-isRODC" in msg and msg["msDS-isRODC"][0] == "TRUE": +            self.dsa_is_ro = True +        else: +            self.dsa_is_ro = False + +        if "msDS-Behavior-Version" in msg: +            self.dsa_behavior = int(msg['msDS-Behavior-Version'][0]) + +        # Load the NC replicas that are enumerated on this dsa +        self.load_replica_table(samdb) + +        # Load the nTDSConnection that are enumerated on this dsa +        self.load_connection_table(samdb) + +        return + + +    def load_replica_table(self, samdb): +        """Method to load the NC replica's listed for DSA object. This +           method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs, +           hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, +           and msDS-HasInstantiatedNCs) to determine complete list of +           NC replicas that are enumerated for the DSA.  Once a NC +           replica is loaded it is identified (schema, config, etc) and +           the other replica attributes (partial, ro, etc) are determined. +           Raises an Exception on error. +           :param samdb: database to query for DSA replica list +        """ +        controls = ["extended_dn:1:1"] +	ncattrs = [ # not RODC - default, config, schema (old style) +                    "hasMasterNCs", +                    # not RODC - default, config, schema, app NCs +                    "msDS-hasMasterNCs", +                    # domain NC partial replicas +                    "hasPartialReplicANCs", +                    # default domain NC +                    "msDS-HasDomainNCs", +                    # RODC only - default, config, schema, app NCs +                    "msDS-hasFullReplicaNCs", +                    # Identifies if replica is coming, going, or stable +                    "msDS-HasInstantiatedNCs" ] +        try: +            res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE, +                               attrs=ncattrs, controls=controls) + +        except ldb.LdbError, (enum, estr): +            raise Exception("Unable to find nTDSDSA NCs for (%s) - (%s)" % \ +                            (self.dsa_dnstr, estr)) +            return + +        # The table of NCs for the dsa we are searching +        tmp_table = {} + +        # We should get one response to our query here for +        # the ntds that we requested +        if len(res[0]) > 0: + +            # Our response will contain a number of elements including +            # the dn of the dsa as well as elements for each +            # attribute (e.g. hasMasterNCs).  Each of these elements +            # is a dictonary list which we retrieve the keys for and +            # then iterate over them +            for k in res[0].keys(): +                if k == "dn": +                    continue + +                # For each attribute type there will be one or more DNs +                # listed.  For instance DCs normally have 3 hasMasterNCs +                # listed. +                for value in res[0][k]: +                    # Turn dn into a dsdb_Dn so we can use +                    # its methods to parse the extended pieces. +                    # Note we don't really need the exact sid value +                    # but instead only need to know if its present. +                    dsdn  = dsdb_Dn(samdb, value) +                    guid  = dsdn.dn.get_extended_component('GUID') +                    sid   = dsdn.dn.get_extended_component('SID') +                    flags = dsdn.get_binary_integer() +                    dnstr = str(dsdn.dn) + +                    if guid is None: +                        raise Exception("Missing GUID for (%s) - (%s: %s)" % \ +                                        (self.dsa_dnstr, k, value)) +                    else: +                        guidstr = str(misc.GUID(guid)) + +                    if not dnstr in tmp_table: +                        rep = NCReplica(self.dsa_dnstr, self.dsa_guid, +                                        dnstr, guidstr, sid) +                        tmp_table[dnstr] = rep +                    else: +                        rep = tmp_table[dnstr] + +                    if k == "msDS-HasInstantiatedNCs": +                        rep.set_replica_flags(flags) +                        continue + +                    rep.identify_by_dsa_attr(samdb, k) + +                    # if we've identified the default domain NC +                    # then save its DN string +                    if rep.is_default(): +                       self.default_dnstr = dnstr +        else: +            raise Exception("No nTDSDSA NCs for (%s)" % self.dsa_dnstr) +            return + +        # Assign our newly built NC replica table to this dsa +        self.rep_table = tmp_table +        return + +    def load_connection_table(self, samdb): +        """Method to load the nTDSConnections listed for DSA object. +           Raises an Exception on error. +           :param samdb: database to query for DSA connection list +        """ +        try: +            res = samdb.search(base=self.dsa_dnstr, +                               scope=ldb.SCOPE_SUBTREE, +                               expression="(objectClass=nTDSConnection)") + +        except ldb.LdbError, (enum, estr): +            raise Exception("Unable to find nTDSConnection for (%s) - (%s)" % \ +                            (self.dsa_dnstr, estr)) +            return + +        for msg in res: +            dnstr = str(msg.dn) + +            # already loaded +            if dnstr in self.connect_table.keys(): +                continue + +            connect = NTDSConnection(dnstr) + +            connect.load_connection(samdb) +            self.connect_table[dnstr] = connect +        return + +    def commit_connection_table(self, samdb): +        """Method to commit any uncommitted nTDSConnections +           that are in our table.  These would be newly identified +           connections that are marked as (committed = False) +           :param samdb: database to commit DSA connection list to +        """ +        for dnstr, connect in self.connect_table.items(): +            connect.commit_connection(samdb) + +    def add_connection_by_dnstr(self, dnstr, connect): +        self.connect_table[dnstr] = connect +        return + +    def get_connection_by_from_dnstr(self, from_dnstr): +        """Scan DSA nTDSConnection table and return connection +           with a "fromServer" dn string equivalent to method +           input parameter. +           :param from_dnstr: search for this from server entry +        """ +        for dnstr, connect in self.connect_table.items(): +            if connect.get_from_dnstr() == from_dnstr: +                return connect +        return None + +    def dumpstr_replica_table(self): +        '''Debug dump string output of replica table''' +        text="" +        for k in self.rep_table.keys(): +            if text: +                text = text + "\n%s" % self.rep_table[k] +            else: +                text = "%s" % self.rep_table[k] +        return text + +    def dumpstr_connect_table(self): +        '''Debug dump string output of connect table''' +        text="" +        for k in self.connect_table.keys(): +            if text: +                text = text + "\n%s" % self.connect_table[k] +            else: +                text = "%s" % self.connect_table[k] +        return text + +class NTDSConnection(): +    """Class defines a nTDSConnection found under a DSA +    """ +    def __init__(self, dnstr): +        self.dnstr       = dnstr +        self.enabled     = False +        self.committed   = False # appears in database +        self.options     = 0 +        self.flags       = 0 +        self.from_dnstr  = None +        self.schedulestr = None +        return + +    def __str__(self): +        '''Debug dump string output of NTDSConnection object''' +        text = "%s: %s" % (self.__class__.__name__, self.dnstr) +        text = text + "\n\tenabled: %s" % self.enabled +        text = text + "\n\tcommitted: %s" % self.committed +        text = text + "\n\toptions: 0x%08X" % self.options +        text = text + "\n\tflags: 0x%08X" % self.flags +        text = text + "\n\tfrom_dn: %s" % self.from_dnstr +        return text + +    def load_connection(self, samdb): +        """Given a NTDSConnection object with an prior initialization +           for the object's DN, search for the DN and load attributes +           from the samdb. +           Raises an Exception on error. +        """ +	attrs = [ "options", +                  "enabledConnection", +                  "schedule", +                  "fromServer", +                  "systemFlags" ] +        try: +            res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE, +                               attrs=attrs) + +        except ldb.LdbError, (enum, estr): +            raise Exception("Unable to find nTDSConnection for (%s) - (%s)" % \ +                            (self.dnstr, estr)) +            return + +        msg = res[0] + +        if "options" in msg: +            self.options = int(msg["options"][0]) +        if "enabledConnection" in msg: +            if msg["enabledConnection"][0].upper().lstrip().rstrip() == "TRUE": +                self.enabled = True +	if "systemFlags" in msg: +            self.flags = int(msg["systemFlags"][0]) +        if "schedule" in msg: +            self.schedulestr = msg["schedule"][0] +        if "fromServer" in msg: +            dsdn = dsdb_Dn(samdb, msg["fromServer"][0]) +            self.from_dnstr = str(dsdn.dn) +            assert self.from_dnstr != None + +        # Appears as committed in the database +        self.committed = True +        return + +    def commit_connection(self, samdb): +        """Given a NTDSConnection object that is not committed in the +           sam database, perform a commit action. +        """ +        if self.committed: # nothing to do +            return + +        # XXX - not yet written +        return + +    def get_from_dnstr(self): +        '''Return fromServer dn string attribute''' +        return self.from_dnstr + +class Partition(NamingContext): +    """Class defines a naming context discovered thru the +       Partitions DN of the configuration schema.  This is +       a more specific form of NamingContext class (inheriting +       from that class) and it identifies unique attributes +       enumerated in the Partitions such as which nTDSDSAs +       are cross referenced for replicas +    """ +    def __init__(self, partstr): +        self.partstr          = partstr +        self.rw_location_list = [] +        self.ro_location_list = [] + +        # We don't have enough info to properly +        # fill in the naming context yet.  We'll get that +        # fully set up with load_partition(). +        NamingContext.__init__(self, None) + + +    def load_partition(self, samdb): +        """Given a Partition class object that has been initialized +           with its partition dn string, load the partition from the +           sam database, identify the type of the partition (schema, +           domain, etc) and record the list of nTDSDSAs that appear +           in the cross reference attributes msDS-NC-Replica-Locations +           and msDS-NC-RO-Replica-Locations. +           Raises an Exception on error. +           :param samdb: sam database to load partition from +        """ +        controls = ["extended_dn:1:1"] +	attrs = [ "nCName", +                  "msDS-NC-Replica-Locations", +                  "msDS-NC-RO-Replica-Locations" ] +        try: +            res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE, +                               attrs=attrs, controls=controls) + +        except ldb.LdbError, (enum, estr): +            raise Exception("Unable to find partition for (%s) - (%s)" % ( +                            self.partstr, estr)) +            return + +        msg = res[0] +        for k in msg.keys(): +            if k == "dn": +                continue + +            for value in msg[k]: +                # Turn dn into a dsdb_Dn so we can use +                # its methods to parse the extended pieces. +                # Note we don't really need the exact sid value +                # but instead only need to know if its present. +                dsdn  = dsdb_Dn(samdb, value) +                guid  = dsdn.dn.get_extended_component('GUID') +                sid   = dsdn.dn.get_extended_component('SID') + +                if guid is None: +                    raise Exception("Missing GUID for (%s) - (%s: %s)" % \ +                                    (self.partstr, k, value)) +                else: +                    guidstr = str(misc.GUID(guid)) + +                if k == "nCName": +                    self.nc_dnstr = str(dsdn.dn) +                    self.nc_guid  = guidstr +                    self.nc_sid   = sid +                    continue + +                if k == "msDS-NC-Replica-Locations": +                    self.rw_location_list.append(str(dsdn.dn)) +                    continue + +                if k == "msDS-NC-RO-Replica-Locations": +                    self.ro_location_list.append(str(dsdn.dn)) +                    continue + +        # Now identify what type of NC this partition +        # enumerated +        self.identify_by_basedn(samdb) + +        return + +    def should_be_present(self, target_dsa): +        """Tests whether this partition should have an NC replica +           on the target dsa.  This method returns a tuple of +           needed=True/False, ro=True/False, partial=True/False +           :param target_dsa: should NC be present on target dsa +        """ +        needed  = False +        ro      = False +        partial = False + +        # If this is the config, schema, or default +        # domain NC for the target dsa then it should +        # be present +        if self.nc_type == NCType.config or \ +           self.nc_type == NCType.schema or \ +           (self.nc_type == NCType.domain and \ +            self.nc_dnstr == target_dsa.default_dnstr): +            needed = True + +        # A writable replica of an application NC should be present +        # if there a cross reference to the target DSA exists.  Depending +        # on whether the DSA is ro we examine which type of cross reference +        # to look for (msDS-NC-Replica-Locations or +        # msDS-NC-RO-Replica-Locations +        if self.nc_type == NCType.application: +            if target_dsa.is_ro(): +               if target_dsa.dsa_dnstr in self.ro_location_list: +                   needed = True +            else: +               if target_dsa.dsa_dnstr in self.rw_location_list: +                   needed = True + +        # If the target dsa is a gc then a partial replica of a +        # domain NC (other than the DSAs default domain) should exist +        # if there is also a cross reference for the DSA +        if target_dsa.is_gc() and \ +           self.nc_type == NCType.domain and \ +           self.nc_dnstr != target_dsa.default_dnstr and \ +           (target_dsa.dsa_dnstr in self.ro_location_list or \ +            target_dsa.dsa_dnstr in self.rw_location_list): +            needed  = True +            partial = True + +        # partial NCs are always readonly +        if needed and (target_dsa.is_ro() or partial): +            ro = True + +        return needed, ro, partial + +    def __str__(self): +        '''Debug dump string output of class''' +        text = "%s" % NamingContext.__str__(self) +        text = text + "\n\tpartdn=%s" % self.partstr +        for k in self.rw_location_list: +            text = text + "\n\tmsDS-NC-Replica-Locations=%s" % k +        for k in self.ro_location_list: +            text = text + "\n\tmsDS-NC-RO-Replica-Locations=%s" % k +        return text + +class Site: +    def __init__(self, site_dnstr): +        self.site_dnstr   = site_dnstr +        self.site_options = 0 +        return + +    def load_site(self, samdb): +        """Loads the NTDS Site Settions options attribute for the site +           Raises an Exception on error. +        """ +        ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr +        try: +            res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE, +                               attrs=["options"]) +        except ldb.LdbError, (enum, estr): +            raise Exception("Unable to find site settings for (%s) - (%s)" % \ +                            (ssdn, estr)) +            return + +        msg = res[0] +	if "options" in msg: +            self.site_options = int(msg["options"][0]) +        return + +    def is_same_site(self, target_dsa): +        '''Determine if target dsa is in this site''' +        if self.site_dnstr in target_dsa.dsa_dnstr: +            return True +        return False + +    def is_intrasite_topology_disabled(self): +        '''Returns True if intrasite topology is disabled for site''' +        if (self.site_options & \ +            dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0: +            return True +        return False + +    def should_detect_stale(self): +        '''Returns True if detect stale is enabled for site''' +        if (self.site_options & \ +            dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) == 0: +            return True +        return False + + +class GraphNode: +    """This is a graph node describing a set of edges that should be +       directed to it.  Each edge is a connection for a particular +       naming context replica directed from another node in the forest +       to this node. +    """ +    def __init__(self, dsa_dnstr, max_node_edges): +        """Instantiate the graph node according to a DSA dn string +           :param max_node_edges: maximum number of edges that should ever +                                  be directed to the node +        """ +        self.max_edges = max_node_edges +        self.dsa_dnstr = dsa_dnstr +        self.edge_from = [] + +    def __str__(self): +        text = "%s: %s" % (self.__class__.__name__, self.dsa_dnstr) +        for edge in self.edge_from: +            text = text + "\n\tedge from: %s" % edge +        return text + +    def add_edge_from(self, from_dsa_dnstr): +        """Add an edge from the dsa to our graph nodes edge from list +           :param from_dsa_dnstr: the dsa that the edge emanates from +        """ +        assert from_dsa_dnstr != None + +        # No edges from myself to myself +        if from_dsa_dnstr == self.dsa_dnstr: +            return False +        # Only one edge from a particular node +        if from_dsa_dnstr in self.edge_from: +            return False +        # Not too many edges +        if len(self.edge_from) >= self.max_edges: +            return False +        self.edge_from.append(from_dsa_dnstr) +        return True + +    def add_edges_from_connections(self, dsa): +        """For each nTDSConnection object associated with a particular +           DSA, we test if it implies an edge to this graph node (i.e. +           the "fromServer" attribute).  If it does then we add an +           edge from the server unless we are over the max edges for this +           graph node +           :param dsa: dsa with a dnstr equivalent to his graph node +        """ +        for dnstr, connect in dsa.connect_table.items(): +            self.add_edge_from(connect.from_dnstr) +        return + +    def add_connections_from_edges(self, dsa): +        """For each edge directed to this graph node, ensure there +           is a corresponding nTDSConnection object in the dsa. +        """ +        for edge_dnstr in self.edge_from: +            connect = dsa.get_connection_by_from_dnstr(edge_dnstr) + +            # For each edge directed to the NC replica that +            # "should be present" on the local DC, the KCC determines +            # whether an object c exists such that: +            # +            #    c is a child of the DC's nTDSDSA object. +            #    c.objectCategory = nTDSConnection +            # +            # Given the NC replica ri from which the edge is directed, +            #    c.fromServer is the dsname of the nTDSDSA object of +            #    the DC on which ri "is present". +            # +            #    c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY +            if connect and \ +               connect.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0: +                exists = True +            else: +                exists = False + +            # if no such object exists then the KCC adds an object +            # c with the following attributes +            if exists: +                return + +            # Generate a new dnstr for this nTDSConnection +            dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr + +            connect = NTDSConnection(dnstr) +            connect.enabled    = True +            connect.committed  = False +            connect.from_dnstr = edge_dnstr +            connect.options    = dsdb.NTDSCONN_OPT_IS_GENERATED +            connect.flags      = dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME + \ +                                 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE + +            # XXX I need to write the schedule blob + +            dsa.add_connection_by_dnstr(dnstr, connect); + +        return + +    def has_sufficient_edges(self): +        '''Return True if we have met the maximum "from edges" criteria''' +        if len(self.edge_from) >= self.max_edges: +            return True +        return False  | 
