Merge tag 'upstream/4.0.5+dfsg1' into samba_4.0_ivo
[abartlet/samba-debian.git] / python / samba / kcc_utils.py
diff --git a/python/samba/kcc_utils.py b/python/samba/kcc_utils.py
new file mode 100644 (file)
index 0000000..57c3187
--- /dev/null
@@ -0,0 +1,2182 @@
+# KCC topology utilities
+#
+# Copyright (C) Dave Craft 2011
+# Copyright (C) Jelmer Vernooij 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 ldb
+import uuid
+import time
+
+from samba import dsdb, unix2nttime
+from samba.dcerpc import (
+    drsblobs,
+    drsuapi,
+    misc,
+    )
+from samba.common import dsdb_Dn
+from samba.ndr import (ndr_unpack, ndr_pack)
+
+
+class NCType(object):
+    (unknown, schema, domain, config, application) = range(0, 5)
+
+
+class NamingContext(object):
+    """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):
+        """Instantiate a NamingContext
+
+        :param nc_dnstr: NC dn string
+        """
+        self.nc_dnstr = nc_dnstr
+        self.nc_guid = None
+        self.nc_sid = None
+        self.nc_type = NCType.unknown
+
+    def __str__(self):
+        '''Debug dump string output of class'''
+        text = "%s:" % self.__class__.__name__
+        text = text + "\n\tnc_dnstr=%s" % self.nc_dnstr
+        text = text + "\n\tnc_guid=%s"  % str(self.nc_guid)
+
+        if self.nc_sid is None:
+            text = text + "\n\tnc_sid=<absent>"
+        else:
+            text = text + "\n\tnc_sid=<present>"
+
+        text = text + "\n\tnc_type=%s"  % self.nc_type
+        return text
+
+    def load_nc(self, samdb):
+        attrs = [ "objectGUID",
+                  "objectSid" ]
+        try:
+            res = samdb.search(base=self.nc_dnstr,
+                               scope=ldb.SCOPE_BASE, attrs=attrs)
+
+        except ldb.LdbError, (enum, estr):
+            raise Exception("Unable to find naming context (%s)" %
+                            (self.nc_dnstr, estr))
+        msg = res[0]
+        if "objectGUID" in msg:
+            self.nc_guid = misc.GUID(samdb.schema_format_value("objectGUID",
+                                     msg["objectGUID"][0]))
+        if "objectSid" in msg:
+            self.nc_sid = msg["objectSid"][0]
+
+        assert self.nc_guid is not None
+
+    def is_schema(self):
+        '''Return True if NC is schema'''
+        assert self.nc_type != NCType.unknown
+        return self.nc_type == NCType.schema
+
+    def is_domain(self):
+        '''Return True if NC is domain'''
+        assert self.nc_type != NCType.unknown
+        return self.nc_type == NCType.domain
+
+    def is_application(self):
+        '''Return True if NC is application'''
+        assert self.nc_type != NCType.unknown
+        return self.nc_type == NCType.application
+
+    def is_config(self):
+        '''Return True if NC is config'''
+        assert self.nc_type != NCType.unknown
+        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
+        """
+        # Invoke loader to initialize guid and more
+        # importantly sid value (sid is used to identify
+        # domain NCs)
+        if self.nc_guid is None:
+            self.load_nc(samdb)
+
+        # 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 is not None:
+            self.nc_type = NCType.domain
+        else:
+            self.nc_type = NCType.application
+
+    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_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)
+
+
+class NCReplica(NamingContext):
+    """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):
+        """Instantiate a Naming Context Replica
+
+        :param dsa_guid: GUID of DSA where replica appears
+        :param nc_dnstr: NC dn string
+        """
+        self.rep_dsa_dnstr = dsa_dnstr
+        self.rep_dsa_guid = dsa_guid
+        self.rep_default = False # replica for DSA's default domain
+        self.rep_partial = False
+        self.rep_ro = False
+        self.rep_instantiated_flags = 0
+
+        self.rep_fsmo_role_owner = None
+
+        # RepsFromTo tuples
+        self.rep_repsFrom = []
+
+        # 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)
+
+    def __str__(self):
+        '''Debug dump string output of class'''
+        text = "%s:" % self.__class__.__name__
+        text = text + "\n\tdsa_dnstr=%s"       % self.rep_dsa_dnstr
+        text = text + "\n\tdsa_guid=%s"        % str(self.rep_dsa_guid)
+        text = text + "\n\tdefault=%s"         % self.rep_default
+        text = text + "\n\tro=%s"              % self.rep_ro
+        text = text + "\n\tpartial=%s"         % self.rep_partial
+        text = text + "\n\tpresent=%s"         % self.is_present()
+        text = text + "\n\tfsmo_role_owner=%s" % self.rep_fsmo_role_owner
+
+        for rep in self.rep_repsFrom:
+            text = text + "\n%s" % rep
+
+        return "%s\n%s" % (NamingContext.__str__(self), text)
+
+    def set_instantiated_flags(self, flags=None):
+        '''Set or clear NC replica instantiated flags'''
+        if flags is None:
+            self.rep_instantiated_flags = 0
+        else:
+            self.rep_instantiated_flags = flags
+
+    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)
+
+    def is_default(self):
+        """Whether this is a default domain 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_instantiated_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0:
+            return True
+        return False
+
+    def load_repsFrom(self, samdb):
+        """Given an NC replica which has been discovered thru the nTDSDSA
+        database object, load the repsFrom attribute for the local replica.
+        held by my dsa.  The repsFrom attribute is not replicated so this
+        attribute is relative only to the local DSA that the samdb exists on
+        """
+        try:
+            res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
+                               attrs=[ "repsFrom" ])
+
+        except ldb.LdbError, (enum, estr):
+            raise Exception("Unable to find NC for (%s) - (%s)" %
+                            (self.nc_dnstr, estr))
+
+        msg = res[0]
+
+        # Possibly no repsFrom if this is a singleton DC
+        if "repsFrom" in msg:
+            for value in msg["repsFrom"]:
+                rep = RepsFromTo(self.nc_dnstr,
+                                 ndr_unpack(drsblobs.repsFromToBlob, value))
+                self.rep_repsFrom.append(rep)
+
+    def commit_repsFrom(self, samdb, ro=False):
+        """Commit repsFrom to the database"""
+
+        # XXX - This is not truly correct according to the MS-TECH
+        #       docs.  To commit a repsFrom we should be using RPCs
+        #       IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
+        #       IDL_DRSReplicaDel to affect a repsFrom change.
+        #
+        #       Those RPCs are missing in samba, so I'll have to
+        #       implement them to get this to more accurately
+        #       reflect the reference docs.  As of right now this
+        #       commit to the database will work as its what the
+        #       older KCC also did
+        modify = False
+        newreps = []
+        delreps = []
+
+        for repsFrom in self.rep_repsFrom:
+
+            # Leave out any to be deleted from
+            # replacement list.  Build a list
+            # of to be deleted reps which we will
+            # remove from rep_repsFrom list below
+            if repsFrom.to_be_deleted:
+                delreps.append(repsFrom)
+                modify = True
+                continue
+
+            if repsFrom.is_modified():
+                repsFrom.set_unmodified()
+                modify = True
+
+            # current (unmodified) elements also get
+            # appended here but no changes will occur
+            # unless something is "to be modified" or
+            # "to be deleted"
+            newreps.append(ndr_pack(repsFrom.ndr_blob))
+
+        # Now delete these from our list of rep_repsFrom
+        for repsFrom in delreps:
+            self.rep_repsFrom.remove(repsFrom)
+        delreps = []
+
+        # Nothing to do if no reps have been modified or
+        # need to be deleted or input option has informed
+        # us to be "readonly" (ro).  Leave database
+        # record "as is"
+        if not modify or ro:
+            return
+
+        m = ldb.Message()
+        m.dn = ldb.Dn(samdb, self.nc_dnstr)
+
+        m["repsFrom"] = \
+            ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsFrom")
+
+        try:
+            samdb.modify(m)
+
+        except ldb.LdbError, estr:
+            raise Exception("Could not set repsFrom for (%s) - (%s)" %
+                            (self.dsa_dnstr, estr))
+
+    def dumpstr_to_be_deleted(self):
+        text=""
+        for repsFrom in self.rep_repsFrom:
+            if repsFrom.to_be_deleted:
+                if text:
+                    text = text + "\n%s" % repsFrom
+                else:
+                    text = "%s" % repsFrom
+        return text
+
+    def dumpstr_to_be_modified(self):
+        text=""
+        for repsFrom in self.rep_repsFrom:
+            if repsFrom.is_modified():
+                if text:
+                    text = text + "\n%s" % repsFrom
+                else:
+                    text = "%s" % repsFrom
+        return text
+
+    def load_fsmo_roles(self, samdb):
+        """Given an NC replica which has been discovered thru the nTDSDSA
+        database object, load the fSMORoleOwner attribute.
+        """
+        try:
+            res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
+                               attrs=[ "fSMORoleOwner" ])
+
+        except ldb.LdbError, (enum, estr):
+            raise Exception("Unable to find NC for (%s) - (%s)" %
+                            (self.nc_dnstr, estr))
+
+        msg = res[0]
+
+        # Possibly no fSMORoleOwner
+        if "fSMORoleOwner" in msg:
+            self.rep_fsmo_role_owner = msg["fSMORoleOwner"]
+
+    def is_fsmo_role_owner(self, dsa_dnstr):
+        if self.rep_fsmo_role_owner is not None and \
+           self.rep_fsmo_role_owner == dsa_dnstr:
+            return True
+        return False
+
+
+class DirectoryServiceAgent(object):
+
+    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_istg = False
+        self.dsa_options = 0
+        self.dsa_behavior = 0
+        self.default_dnstr = None  # default domain dn string for dsa
+
+        # NCReplicas for this dsa that are "present"
+        # Indexed by DN string of naming context
+        self.current_rep_table = {}
+
+        # NCReplicas for this dsa that "should be present"
+        # Indexed by DN string of naming context
+        self.needed_rep_table = {}
+
+        # NTDSConnections for this dsa.  These are current
+        # valid connections that are committed or pending a commit
+        # in the database.  Indexed by DN string of connection
+        self.connect_table = {}
+
+    def __str__(self):
+        '''Debug dump string output of class'''
+
+        text = "%s:" % self.__class__.__name__
+        if self.dsa_dnstr is not None:
+            text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
+        if self.dsa_guid is not None:
+            text = text + "\n\tdsa_guid=%s"  % str(self.dsa_guid)
+        if self.dsa_ivid is not None:
+            text = text + "\n\tdsa_ivid=%s"  % str(self.dsa_ivid)
+
+        text = text + "\n\tro=%s" % self.is_ro()
+        text = text + "\n\tgc=%s" % self.is_gc()
+        text = text + "\n\tistg=%s" % self.is_istg()
+
+        text = text + "\ncurrent_replica_table:"
+        text = text + "\n%s" % self.dumpstr_current_replica_table()
+        text = text + "\nneeded_replica_table:"
+        text = text + "\n%s" % self.dumpstr_needed_replica_table()
+        text = text + "\nconnect_table:"
+        text = text + "\n%s" % self.dumpstr_connect_table()
+
+        return text
+
+    def get_current_replica(self, nc_dnstr):
+        if nc_dnstr in self.current_rep_table.keys():
+            return self.current_rep_table[nc_dnstr]
+        else:
+            return None
+
+    def is_istg(self):
+        '''Returns True if dsa is intersite topology generator for it's site'''
+        # The KCC on an RODC always acts as an ISTG for itself
+        return self.dsa_is_istg or self.dsa_is_ro
+
+    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'''
+        if (self.options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
+            return True
+        return False
+
+    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 is_translate_ntdsconn_disabled(self):
+        """Whether this allows NTDSConnection translation in its options."""
+        if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0:
+            return True
+        return False
+
+    def get_rep_tables(self):
+        """Return DSA current and needed replica tables
+        """
+        return self.current_rep_table, self.needed_rep_table
+
+    def get_parent_dnstr(self):
+        """Get the parent DN string of this object."""
+        head, sep, tail = self.dsa_dnstr.partition(',')
+        return tail
+
+    def load_dsa(self, samdb):
+        """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.
+        """
+        attrs = ["objectGUID",
+                 "invocationID",
+                 "options",
+                 "msDS-isRODC",
+                 "msDS-Behavior-Version"]
+        try:
+            res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
+                               attrs=attrs)
+
+        except ldb.LdbError, (enum, estr):
+            raise Exception("Unable to find nTDSDSA for (%s) - (%s)" %
+                            (self.dsa_dnstr, estr))
+
+        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:
+            self.options = int(msg["options"][0])
+
+        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_current_replica_table(samdb)
+
+        # Load the nTDSConnection that are enumerated on this dsa
+        self.load_connection_table(samdb)
+
+    def load_current_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.
+
+        :param samdb: database to query for DSA replica list
+        """
+        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)
+
+        except ldb.LdbError, (enum, estr):
+            raise Exception("Unable to find nTDSDSA NCs for (%s) - (%s)" %
+                            (self.dsa_dnstr, estr))
+
+        # 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 a binary DN
+                    dsdn = dsdb_Dn(samdb, value)
+                    flags = dsdn.get_binary_integer()
+                    dnstr = str(dsdn.dn)
+
+                    if not dnstr in tmp_table.keys():
+                        rep = NCReplica(self.dsa_dnstr, self.dsa_guid, dnstr)
+                        tmp_table[dnstr] = rep
+                    else:
+                        rep = tmp_table[dnstr]
+
+                    if k == "msDS-HasInstantiatedNCs":
+                        rep.set_instantiated_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)
+
+        # Assign our newly built NC replica table to this dsa
+        self.current_rep_table = tmp_table
+
+    def add_needed_replica(self, rep):
+        """Method to add a NC replica that "should be present" to the
+        needed_rep_table if not already in the table
+        """
+        if not rep.nc_dnstr in self.needed_rep_table.keys():
+            self.needed_rep_table[rep.nc_dnstr] = rep
+
+    def load_connection_table(self, samdb):
+        """Method to load the nTDSConnections listed for DSA object.
+
+        :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))
+
+        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
+
+    def commit_connections(self, samdb, ro=False):
+        """Method to commit any uncommitted nTDSConnections
+        modifications that are in our table.  These would be
+        identified connections that are marked to be added or
+        deleted
+
+        :param samdb: database to commit DSA connection list to
+        :param ro: if (true) then peform internal operations but
+            do not write to the database (readonly)
+        """
+        delconn = []
+
+        for dnstr, connect in self.connect_table.items():
+            if connect.to_be_added:
+                connect.commit_added(samdb, ro)
+
+            if connect.to_be_modified:
+                connect.commit_modified(samdb, ro)
+
+            if connect.to_be_deleted:
+                connect.commit_deleted(samdb, ro)
+                delconn.append(dnstr)
+
+        # Now delete the connection from the table
+        for dnstr in delconn:
+            del self.connect_table[dnstr]
+
+    def add_connection(self, dnstr, connect):
+        assert dnstr not in self.connect_table.keys()
+        self.connect_table[dnstr] = connect
+
+    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_current_replica_table(self):
+        '''Debug dump string output of current replica table'''
+        text=""
+        for k in self.current_rep_table.keys():
+            if text:
+                text = text + "\n%s" % self.current_rep_table[k]
+            else:
+                text = "%s" % self.current_rep_table[k]
+        return text
+
+    def dumpstr_needed_replica_table(self):
+        '''Debug dump string output of needed replica table'''
+        text=""
+        for k in self.needed_rep_table.keys():
+            if text:
+                text = text + "\n%s" % self.needed_rep_table[k]
+            else:
+                text = "%s" % self.needed_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
+
+    def new_connection(self, options, flags, transport, from_dnstr, sched):
+        """Set up a new connection for the DSA based on input
+        parameters.  Connection will be added to the DSA
+        connect_table and will be marked as "to be added" pending
+        a call to commit_connections()
+        """
+        dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
+
+        connect = NTDSConnection(dnstr)
+        connect.to_be_added = True
+        connect.enabled = True
+        connect.from_dnstr = from_dnstr
+        connect.options = options
+        connect.flags = flags
+
+        if transport is not None:
+            connect.transport_dnstr = transport.dnstr
+
+        if sched is not None:
+            connect.schedule = sched
+        else:
+            # Create schedule.  Attribute valuse set according to MS-TECH
+            # intrasite connection creation document
+            connect.schedule = drsblobs.schedule()
+
+            connect.schedule.size = 188
+            connect.schedule.bandwidth = 0
+            connect.schedule.numberOfSchedules = 1
+
+            header = drsblobs.scheduleHeader()
+            header.type = 0
+            header.offset = 20
+
+            connect.schedule.headerArray = [ header ]
+
+            # 168 byte instances of the 0x01 value.  The low order 4 bits
+            # of the byte equate to 15 minute intervals within a single hour.
+            # There are 168 bytes because there are 168 hours in a full week
+            # Effectively we are saying to perform replication at the end of
+            # each hour of the week
+            data = drsblobs.scheduleSlots()
+            data.slots = [ 0x01 ] * 168
+
+            connect.schedule.dataArray = [ data ]
+
+        self.add_connection(dnstr, connect);
+        return connect
+
+
+class NTDSConnection(object):
+    """Class defines a nTDSConnection found under a DSA
+    """
+    def __init__(self, dnstr):
+        self.dnstr = dnstr
+        self.guid = None
+        self.enabled = False
+        self.whenCreated = 0
+        self.to_be_added = False # new connection needs to be added
+        self.to_be_deleted = False # old connection needs to be deleted
+        self.to_be_modified = False
+        self.options = 0
+        self.system_flags = 0
+        self.transport_dnstr = None
+        self.transport_guid = None
+        self.from_dnstr = None
+        self.schedule = None
+
+    def __str__(self):
+        '''Debug dump string output of NTDSConnection object'''
+
+        text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
+        text = text + "\n\tenabled=%s" % self.enabled
+        text = text + "\n\tto_be_added=%s" % self.to_be_added
+        text = text + "\n\tto_be_deleted=%s" % self.to_be_deleted
+        text = text + "\n\tto_be_modified=%s" % self.to_be_modified
+        text = text + "\n\toptions=0x%08X" % self.options
+        text = text + "\n\tsystem_flags=0x%08X" % self.system_flags
+        text = text + "\n\twhenCreated=%d" % self.whenCreated
+        text = text + "\n\ttransport_dn=%s" % self.transport_dnstr
+
+        if self.guid is not None:
+            text = text + "\n\tguid=%s" % str(self.guid)
+
+        if self.transport_guid is not None:
+            text = text + "\n\ttransport_guid=%s" % str(self.transport_guid)
+
+        text = text + "\n\tfrom_dn=%s" % self.from_dnstr
+
+        if self.schedule is not None:
+            text = text + "\n\tschedule.size=%s" % self.schedule.size
+            text = text + "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
+            text = text + "\n\tschedule.numberOfSchedules=%s" % \
+                   self.schedule.numberOfSchedules
+
+            for i, header in enumerate(self.schedule.headerArray):
+                text = text + "\n\tschedule.headerArray[%d].type=%d" % \
+                       (i, header.type)
+                text = text + "\n\tschedule.headerArray[%d].offset=%d" % \
+                       (i, header.offset)
+                text = text + "\n\tschedule.dataArray[%d].slots[ " % i
+                for slot in self.schedule.dataArray[i].slots:
+                    text = text + "0x%X " % slot
+                text = text + "]"
+
+        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.
+        """
+        attrs = [ "options",
+                  "enabledConnection",
+                  "schedule",
+                  "whenCreated",
+                  "objectGUID",
+                  "transportType",
+                  "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))
+
+        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.system_flags = int(msg["systemFlags"][0])
+
+        if "objectGUID" in msg:
+            self.guid = \
+                misc.GUID(samdb.schema_format_value("objectGUID",
+                                                    msg["objectGUID"][0]))
+
+        if "transportType" in msg:
+            dsdn = dsdb_Dn(samdb, msg["tranportType"][0])
+            self.load_connection_transport(str(dsdn.dn))
+
+        if "schedule" in msg:
+            self.schedule = ndr_unpack(drsblobs.replSchedule, msg["schedule"][0])
+
+        if "whenCreated" in msg:
+            self.whenCreated = ldb.string_to_time(msg["whenCreated"][0])
+
+        if "fromServer" in msg:
+            dsdn = dsdb_Dn(samdb, msg["fromServer"][0])
+            self.from_dnstr = str(dsdn.dn)
+            assert self.from_dnstr is not None
+
+    def load_connection_transport(self, tdnstr):
+        """Given a NTDSConnection object which enumerates a transport
+        DN, load the transport information for the connection object
+
+        :param tdnstr: transport DN to load
+        """
+        attrs = [ "objectGUID" ]
+        try:
+            res = samdb.search(base=tdnstr,
+                               scope=ldb.SCOPE_BASE, attrs=attrs)
+
+        except ldb.LdbError, (enum, estr):
+            raise Exception("Unable to find transport (%s)" %
+                            (tdnstr, estr))
+
+        if "objectGUID" in res[0]:
+            self.transport_dnstr = tdnstr
+            self.transport_guid = \
+                misc.GUID(samdb.schema_format_value("objectGUID",
+                                                    msg["objectGUID"][0]))
+        assert self.transport_dnstr is not None
+        assert self.transport_guid is not None
+
+    def commit_deleted(self, samdb, ro=False):
+        """Local helper routine for commit_connections() which
+        handles committed connections that are to be deleted from
+        the database database
+        """
+        assert self.to_be_deleted
+        self.to_be_deleted = False
+
+        # No database modification requested
+        if ro:
+            return
+
+        try:
+            samdb.delete(self.dnstr)
+        except ldb.LdbError, (enum, estr):
+            raise Exception("Could not delete nTDSConnection for (%s) - (%s)" %
+                            (self.dnstr, estr))
+
+    def commit_added(self, samdb, ro=False):
+        """Local helper routine for commit_connections() which
+        handles committed connections that are to be added to the
+        database
+        """
+        assert self.to_be_added
+        self.to_be_added = False
+
+        # No database modification requested
+        if ro:
+            return
+
+        # First verify we don't have this entry to ensure nothing
+        # is programatically amiss
+        found = False
+        try:
+            msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
+            if len(msg) != 0:
+                found = True
+
+        except ldb.LdbError, (enum, estr):
+            if enum != ldb.ERR_NO_SUCH_OBJECT:
+                raise Exception("Unable to search for (%s) - (%s)" %
+                                (self.dnstr, estr))
+        if found:
+            raise Exception("nTDSConnection for (%s) already exists!" %
+                            self.dnstr)
+
+        if self.enabled:
+            enablestr = "TRUE"
+        else:
+            enablestr = "FALSE"
+
+        # Prepare a message for adding to the samdb
+        m = ldb.Message()
+        m.dn = ldb.Dn(samdb, self.dnstr)
+
+        m["objectClass"] = \
+            ldb.MessageElement("nTDSConnection", ldb.FLAG_MOD_ADD,
+                               "objectClass")
+        m["showInAdvancedViewOnly"] = \
+            ldb.MessageElement("TRUE", ldb.FLAG_MOD_ADD,
+                               "showInAdvancedViewOnly")
+        m["enabledConnection"] = \
+            ldb.MessageElement(enablestr, ldb.FLAG_MOD_ADD, "enabledConnection")
+        m["fromServer"] = \
+            ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_ADD, "fromServer")
+        m["options"] = \
+            ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options")
+        m["systemFlags"] = \
+            ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_ADD,
+                               "systemFlags")
+
+        if self.transport_dnstr is not None:
+            m["transportType"] = \
+                ldb.MessageElement(str(self.transport_dnstr), ldb.FLAG_MOD_ADD,
+                                   "transportType")
+
+        if self.schedule is not None:
+            m["schedule"] = \
+                ldb.MessageElement(ndr_pack(self.schedule),
+                                   ldb.FLAG_MOD_ADD, "schedule")
+        try:
+            samdb.add(m)
+        except ldb.LdbError, (enum, estr):
+            raise Exception("Could not add nTDSConnection for (%s) - (%s)" %
+                            (self.dnstr, estr))
+
+    def commit_modified(self, samdb, ro=False):
+        """Local helper routine for commit_connections() which
+        handles committed connections that are to be modified to the
+        database
+        """
+        assert self.to_be_modified
+        self.to_be_modified = False
+
+        # No database modification requested
+        if ro:
+            return
+
+        # First verify we have this entry to ensure nothing
+        # is programatically amiss
+        try:
+            msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
+            found = True
+
+        except ldb.LdbError, (enum, estr):
+            if enum == ldb.ERR_NO_SUCH_OBJECT:
+                found = False
+            else:
+                raise Exception("Unable to search for (%s) - (%s)" %
+                                (self.dnstr, estr))
+        if not found:
+            raise Exception("nTDSConnection for (%s) doesn't exist!" %
+                            self.dnstr)
+
+        if self.enabled:
+            enablestr = "TRUE"
+        else:
+            enablestr = "FALSE"
+
+        # Prepare a message for modifying the samdb
+        m = ldb.Message()
+        m.dn = ldb.Dn(samdb, self.dnstr)
+
+        m["enabledConnection"] = \
+            ldb.MessageElement(enablestr, ldb.FLAG_MOD_REPLACE,
+                               "enabledConnection")
+        m["fromServer"] = \
+            ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_REPLACE,
+                               "fromServer")
+        m["options"] = \
+            ldb.MessageElement(str(self.options), ldb.FLAG_MOD_REPLACE,
+                               "options")
+        m["systemFlags"] = \
+            ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_REPLACE,
+                               "systemFlags")
+
+        if self.transport_dnstr is not None:
+            m["transportType"] = \
+                ldb.MessageElement(str(self.transport_dnstr),
+                                   ldb.FLAG_MOD_REPLACE, "transportType")
+        else:
+            m["transportType"] = \
+                ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "transportType")
+
+        if self.schedule is not None:
+            m["schedule"] = \
+                ldb.MessageElement(ndr_pack(self.schedule),
+                                   ldb.FLAG_MOD_REPLACE, "schedule")
+        else:
+            m["schedule"] = \
+                ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "schedule")
+        try:
+            samdb.modify(m)
+        except ldb.LdbError, (enum, estr):
+            raise Exception("Could not modify nTDSConnection for (%s) - (%s)" %
+                            (self.dnstr, estr))
+
+    def set_modified(self, truefalse):
+        self.to_be_modified = truefalse
+
+    def set_added(self, truefalse):
+        self.to_be_added = truefalse
+
+    def set_deleted(self, truefalse):
+        self.to_be_deleted = truefalse
+
+    def is_schedule_minimum_once_per_week(self):
+        """Returns True if our schedule includes at least one
+        replication interval within the week.  False otherwise
+        """
+        if self.schedule is None or self.schedule.dataArray[0] is None:
+            return False
+
+        for slot in self.schedule.dataArray[0].slots:
+           if (slot & 0x0F) != 0x0:
+               return True
+        return False
+
+    def is_equivalent_schedule(self, sched):
+        """Returns True if our schedule is equivalent to the input
+        comparison schedule.
+
+        :param shed: schedule to compare to
+        """
+        if self.schedule is not None:
+            if sched is None:
+               return False
+        elif sched is None:
+            return True
+
+        if (self.schedule.size != sched.size or
+            self.schedule.bandwidth != sched.bandwidth or
+            self.schedule.numberOfSchedules != sched.numberOfSchedules):
+            return False
+
+        for i, header in enumerate(self.schedule.headerArray):
+
+            if self.schedule.headerArray[i].type != sched.headerArray[i].type:
+                return False
+
+            if self.schedule.headerArray[i].offset != \
+               sched.headerArray[i].offset:
+                return False
+
+            for a, b in zip(self.schedule.dataArray[i].slots,
+                            sched.dataArray[i].slots):
+                if a != b:
+                    return False
+        return True
+
+    def convert_schedule_to_repltimes(self):
+        """Convert NTDS Connection schedule to replTime schedule.
+
+        NTDS Connection schedule slots are double the size of
+        the replTime slots but the top portion of the NTDS
+        Connection schedule slot (4 most significant bits in
+        uchar) are unused.  The 4 least significant bits have
+        the same (15 minute interval) bit positions as replTimes.
+        We thus pack two elements of the NTDS Connection schedule
+        slots into one element of the replTimes slot
+        If no schedule appears in NTDS Connection then a default
+        of 0x11 is set in each replTimes slot as per behaviour
+        noted in a Windows DC.  That default would cause replication
+        within the last 15 minutes of each hour.
+        """
+        times = [0x11] * 84
+
+        for i, slot in enumerate(times):
+            if self.schedule is not None and \
+               self.schedule.dataArray[0] is not None:
+                slot = (self.schedule.dataArray[0].slots[i*2] & 0xF) << 4 | \
+                       (self.schedule.dataArray[0].slots[i*2] & 0xF)
+        return times
+
+    def is_rodc_topology(self):
+        """Returns True if NTDS Connection specifies RODC
+        topology only
+        """
+        if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
+            return False
+        return True
+
+    def is_generated(self):
+        """Returns True if NTDS Connection was generated by the
+        KCC topology algorithm as opposed to set by the administrator
+        """
+        if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0:
+            return False
+        return True
+
+    def is_override_notify_default(self):
+        """Returns True if NTDS Connection should override notify default
+        """
+        if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0:
+            return False
+        return True
+
+    def is_use_notify(self):
+        """Returns True if NTDS Connection should use notify
+        """
+        if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0:
+            return False
+        return True
+
+    def is_twoway_sync(self):
+        """Returns True if NTDS Connection should use twoway sync
+        """
+        if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0:
+            return False
+        return True
+
+    def is_intersite_compression_disabled(self):
+        """Returns True if NTDS Connection intersite compression
+        is disabled
+        """
+        if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0:
+            return False
+        return True
+
+    def is_user_owned_schedule(self):
+        """Returns True if NTDS Connection has a user owned schedule
+        """
+        if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0:
+            return False
+        return True
+
+    def is_enabled(self):
+        """Returns True if NTDS Connection is enabled
+        """
+        return self.enabled
+
+    def get_from_dnstr(self):
+        '''Return fromServer dn string attribute'''
+        return self.from_dnstr
+
+
+class Partition(NamingContext):
+    """A naming context discovered thru Partitions DN of the config 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.enabled = True
+        self.system_flags = 0
+        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.
+
+        :param samdb: sam database to load partition from
+        """
+        attrs = [ "nCName",
+                  "Enabled",
+                  "systemFlags",
+                  "msDS-NC-Replica-Locations",
+                  "msDS-NC-RO-Replica-Locations" ]
+        try:
+            res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE,
+                               attrs=attrs)
+
+        except ldb.LdbError, (enum, estr):
+            raise Exception("Unable to find partition for (%s) - (%s)" % (
+                            self.partstr, estr))
+
+        msg = res[0]
+        for k in msg.keys():
+            if k == "dn":
+                continue
+
+            if k == "Enabled":
+                if msg[k][0].upper().lstrip().rstrip() == "TRUE":
+                    self.enabled = True
+                else:
+                    self.enabled = False
+                continue
+
+            if k == "systemFlags":
+                self.system_flags = int(msg[k][0])
+                continue
+
+            for value in msg[k]:
+                dsdn = dsdb_Dn(samdb, value)
+                dnstr = str(dsdn.dn)
+
+                if k == "nCName":
+                    self.nc_dnstr = dnstr
+                    continue
+
+                if k == "msDS-NC-Replica-Locations":
+                    self.rw_location_list.append(dnstr)
+                    continue
+
+                if k == "msDS-NC-RO-Replica-Locations":
+                    self.ro_location_list.append(dnstr)
+                    continue
+
+        # Now identify what type of NC this partition
+        # enumerated
+        self.identify_by_basedn(samdb)
+
+    def is_enabled(self):
+        """Returns True if partition is enabled
+        """
+        return self.is_enabled
+
+    def is_foreign(self):
+        """Returns True if this is not an Active Directory NC in our
+        forest but is instead something else (e.g. a foreign NC)
+        """
+        if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0:
+            return True
+        else:
+            return False
+
+    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(object):
+    """An individual site object discovered thru the configuration
+    naming context.  Contains all DSAs that exist within the site
+    """
+    def __init__(self, site_dnstr):
+        self.site_dnstr = site_dnstr
+        self.site_options = 0
+        self.site_topo_generator = None
+        self.site_topo_failover = 0  # appears to be in minutes
+        self.dsa_table = {}
+
+    def load_site(self, samdb):
+        """Loads the NTDS Site Settions options attribute for the site
+        as well as querying and loading all DSAs that appear within
+        the site.
+        """
+        ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
+        attrs = ["options",
+                 "interSiteTopologyFailover",
+                 "interSiteTopologyGenerator"]
+        try:
+            res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE,
+                               attrs=attrs)
+        except ldb.LdbError, (enum, estr):
+            raise Exception("Unable to find site settings for (%s) - (%s)" %
+                            (ssdn, estr))
+
+        msg = res[0]
+        if "options" in msg:
+            self.site_options = int(msg["options"][0])
+
+        if "interSiteTopologyGenerator" in msg:
+            self.site_topo_generator = str(msg["interSiteTopologyGenerator"][0])
+
+        if "interSiteTopologyFailover" in msg:
+            self.site_topo_failover = int(msg["interSiteTopologyFailover"][0])
+
+        self.load_all_dsa(samdb)
+
+    def load_all_dsa(self, samdb):
+        """Discover all nTDSDSA thru the sites entry and
+        instantiate and load the DSAs.  Each dsa is inserted
+        into the dsa_table by dn string.
+        """
+        try:
+            res = samdb.search(self.site_dnstr,
+                               scope=ldb.SCOPE_SUBTREE,
+                               expression="(objectClass=nTDSDSA)")
+        except ldb.LdbError, (enum, estr):
+            raise Exception("Unable to find nTDSDSAs - (%s)" % estr)
+
+        for msg in res:
+            dnstr = str(msg.dn)
+
+            # already loaded
+            if dnstr in self.dsa_table.keys():
+                continue
+
+            dsa = DirectoryServiceAgent(dnstr)
+
+            dsa.load_dsa(samdb)
+
+            # Assign this dsa to my dsa table
+            # and index by dsa dn
+            self.dsa_table[dnstr] = dsa
+
+    def get_dsa_by_guidstr(self, guidstr):
+        for dsa in self.dsa_table.values():
+            if str(dsa.dsa_guid) == guidstr:
+                return dsa
+        return None
+
+    def get_dsa(self, dnstr):
+        """Return a previously loaded DSA object by consulting
+        the sites dsa_table for the provided DSA dn string
+
+        :return: None if DSA doesn't exist
+        """
+        if dnstr in self.dsa_table.keys():
+            return self.dsa_table[dnstr]
+        return None
+
+    def select_istg(self, samdb, mydsa, ro):
+        """Determine if my DC should be an intersite topology
+        generator.  If my DC is the istg and is both a writeable
+        DC and the database is opened in write mode then we perform
+        an originating update to set the interSiteTopologyGenerator
+        attribute in the NTDS Site Settings object.  An RODC always
+        acts as an ISTG for itself.
+        """
+        # The KCC on an RODC always acts as an ISTG for itself
+        if mydsa.dsa_is_ro:
+            mydsa.dsa_is_istg = True
+            return True
+
+        # Find configuration NC replica for my DSA
+        for c_rep in mydsa.current_rep_table.values():
+            if c_rep.is_config():
+                break
+
+        if c_rep is None:
+            raise Exception("Unable to find config NC replica for (%s)" %
+                            mydsa.dsa_dnstr)
+
+        # Load repsFrom if not already loaded so we can get the current
+        # state of the config replica and whether we are getting updates
+        # from the istg
+        c_rep.load_repsFrom(samdb)
+
+        # From MS-Tech ISTG selection:
+        #     First, the KCC on a writable DC determines whether it acts
+        #     as an ISTG for its site
+        #
+        #     Let s be the object such that s!lDAPDisplayName = nTDSDSA
+        #     and classSchema in s!objectClass.
+        #
+        #     Let D be the sequence of objects o in the site of the local
+        #     DC such that o!objectCategory = s. D is sorted in ascending
+        #     order by objectGUID.
+        #
+        # Which is a fancy way of saying "sort all the nTDSDSA objects
+        # in the site by guid in ascending order".   Place sorted list
+        # in D_sort[]
+        D_sort = []
+        d_dsa = None
+
+        unixnow = int(time.time())     # seconds since 1970
+        ntnow = unix2nttime(unixnow) # double word number of 100 nanosecond
+                                       # intervals since 1600s
+
+        for dsa in self.dsa_table.values():
+            D_sort.append(dsa)
+
+        D_sort.sort(sort_dsa_by_guid)
+
+        # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
+        # if o!interSiteTopologyFailover is 0 or has no value.
+        #
+        # Note: lastSuccess and ntnow are in 100 nanosecond intervals
+        #       so it appears we have to turn f into the same interval
+        #
+        #       interSiteTopologyFailover (if set) appears to be in minutes
+        #       so we'll need to convert to senconds and then 100 nanosecond
+        #       intervals
+        #
+        #       10,000,000 is number of 100 nanosecond intervals in a second
+        if self.site_topo_failover == 0:
+            f = 2 * 60 * 60 * 10000000
+        else:
+            f = self.site_topo_failover * 60 * 10000000
+
+        # From MS-Tech ISTG selection:
+        #     If o != NULL and o!interSiteTopologyGenerator is not the
+        #     nTDSDSA object for the local DC and
+        #     o!interSiteTopologyGenerator is an element dj of sequence D:
+        #
+        if self.site_topo_generator is not None and \
+           self.site_topo_generator in self.dsa_table.keys():
+            d_dsa = self.dsa_table[self.site_topo_generator]
+            j_idx = D_sort.index(d_dsa)
+
+        if d_dsa is not None and d_dsa is not mydsa:
+           # From MS-Tech ISTG selection:
+           #     Let c be the cursor in the replUpToDateVector variable
+           #     associated with the NC replica of the config NC such
+           #     that c.uuidDsa = dj!invocationId. If no such c exists
+           #     (No evidence of replication from current ITSG):
+           #         Let i = j.
+           #         Let t = 0.
+           #
+           #     Else if the current time < c.timeLastSyncSuccess - f
+           #     (Evidence of time sync problem on current ISTG):
+           #         Let i = 0.
+           #         Let t = 0.
+           #
+           #     Else (Evidence of replication from current ITSG):
+           #         Let i = j.
+           #         Let t = c.timeLastSyncSuccess.
+           #
+           # last_success appears to be a double word containing
+           #     number of 100 nanosecond intervals since the 1600s
+           if d_dsa.dsa_ivid != c_rep.source_dsa_invocation_id:
+               i_idx = j_idx
+               t_time = 0
+
+           elif ntnow < (c_rep.last_success - f):
+               i_idx = 0
+               t_time = 0
+           else:
+               i_idx = j_idx
+               t_time = c_rep.last_success
+
+        # Otherwise (Nominate local DC as ISTG):
+        #     Let i be the integer such that di is the nTDSDSA
+        #         object for the local DC.
+        #     Let t = the current time.
+        else:
+            i_idx = D_sort.index(mydsa)
+            t_time = ntnow
+
+        # Compute a function that maintains the current ISTG if
+        # it is alive, cycles through other candidates if not.
+        #
+        # Let k be the integer (i + ((current time - t) /
+        #     o!interSiteTopologyFailover)) MOD |D|.
+        #
+        # Note: We don't want to divide by zero here so they must
+        #       have meant "f" instead of "o!interSiteTopologyFailover"
+        k_idx = (i_idx + ((ntnow - t_time) / f)) % len(D_sort)
+
+        # The local writable DC acts as an ISTG for its site if and
+        # only if dk is the nTDSDSA object for the local DC. If the
+        # local DC does not act as an ISTG, the KCC skips the
+        # remainder of this task.
+        d_dsa = D_sort[k_idx]
+        d_dsa.dsa_is_istg = True
+
+        # Update if we are the ISTG, otherwise return
+        if d_dsa is not mydsa:
+            return False
+
+        # Nothing to do
+        if self.site_topo_generator == mydsa.dsa_dnstr:
+            return True
+
+        self.site_topo_generator = mydsa.dsa_dnstr
+
+        # If readonly database then do not perform a
+        # persistent update
+        if ro:
+            return True
+
+        # Perform update to the samdb
+        ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
+
+        m = ldb.Message()
+        m.dn = ldb.Dn(samdb, ssdn)
+
+        m["interSiteTopologyGenerator"] = \
+            ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
+                               "interSiteTopologyGenerator")
+        try:
+            samdb.modify(m)
+
+        except ldb.LdbError, estr:
+            raise Exception(
+                "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
+                (ssdn, estr))
+        return True
+
+    def is_intrasite_topology_disabled(self):
+        '''Returns True if intra-site topology is disabled for site'''
+        if (self.site_options &
+            dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0:
+            return True
+        return False
+
+    def is_intersite_topology_disabled(self):
+        '''Returns True if inter-site topology is disabled for site'''
+        if (self.site_options &
+            dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED) != 0:
+            return True
+        return False
+
+    def is_random_bridgehead_disabled(self):
+        '''Returns True if selection of random bridgehead is disabled'''
+        if (self.site_options &
+            dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0:
+            return True
+        return False
+
+    def is_detect_stale_disabled(self):
+        '''Returns True if detect stale is disabled for site'''
+        if (self.site_options &
+            dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0:
+            return True
+        return False
+
+    def is_cleanup_ntdsconn_disabled(self):
+        '''Returns True if NTDS Connection cleanup is disabled for site'''
+        if (self.site_options &
+            dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0:
+            return True
+        return False
+
+    def same_site(self, dsa):
+       '''Return True if dsa is in this site'''
+       if self.get_dsa(dsa.dsa_dnstr):
+           return True
+       return False
+
+    def __str__(self):
+        '''Debug dump string output of class'''
+        text = "%s:" % self.__class__.__name__
+        text = text + "\n\tdn=%s"             % self.site_dnstr
+        text = text + "\n\toptions=0x%X"      % self.site_options
+        text = text + "\n\ttopo_generator=%s" % self.site_topo_generator
+        text = text + "\n\ttopo_failover=%d"  % self.site_topo_failover
+        for key, dsa in self.dsa_table.items():
+            text = text + "\n%s" % dsa
+        return text
+
+
+class GraphNode(object):
+    """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:" % self.__class__.__name__
+        text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
+        text = text + "\n\tmax_edges=%d" % self.max_edges
+
+        for i, edge in enumerate(self.edge_from):
+            text = text + "\n\tedge_from[%d]=%s" % (i, 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 is not 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)
+
+    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 not connect.is_rodc_topology():
+                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
+            opt = dsdb.NTDSCONN_OPT_IS_GENERATED
+            flags = dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME + \
+                     dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE
+
+            dsa.create_connection(opt, flags, None, edge_dnstr, None)
+
+    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
+
+
+class Transport(object):
+    """Class defines a Inter-site transport found under Sites
+    """
+
+    def __init__(self, dnstr):
+        self.dnstr = dnstr
+        self.options = 0
+        self.guid = None
+        self.name = None
+        self.address_attr = None
+        self.bridgehead_list = []
+
+    def __str__(self):
+        '''Debug dump string output of Transport object'''
+
+        text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
+        text = text + "\n\tguid=%s" % str(self.guid)
+        text = text + "\n\toptions=%d" % self.options
+        text = text + "\n\taddress_attr=%s" % self.address_attr
+        text = text + "\n\tname=%s" % self.name
+        for dnstr in self.bridgehead_list:
+            text = text + "\n\tbridgehead_list=%s" % dnstr
+
+        return text
+
+    def load_transport(self, samdb):
+        """Given a Transport object with an prior initialization
+        for the object's DN, search for the DN and load attributes
+        from the samdb.
+        """
+        attrs = [ "objectGUID",
+                  "options",
+                  "name",
+                  "bridgeheadServerListBL",
+                  "transportAddressAttribute" ]
+        try:
+            res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
+                               attrs=attrs)
+
+        except ldb.LdbError, (enum, estr):
+            raise Exception("Unable to find Transport for (%s) - (%s)" %
+                            (self.dnstr, estr))
+
+        msg = res[0]
+        self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
+                              msg["objectGUID"][0]))
+
+        if "options" in msg:
+            self.options = int(msg["options"][0])
+
+        if "transportAddressAttribute" in msg:
+            self.address_attr = str(msg["transportAddressAttribute"][0])
+
+        if "name" in msg:
+            self.name = str(msg["name"][0])
+
+        if "bridgeheadServerListBL" in msg:
+            for value in msg["bridgeheadServerListBL"]:
+                dsdn = dsdb_Dn(samdb, value)
+                dnstr = str(dsdn.dn)
+                if dnstr not in self.bridgehead_list:
+                    self.bridgehead_list.append(dnstr)
+
+
+class RepsFromTo(object):
+    """Class encapsulation of the NDR repsFromToBlob.
+
+    Removes the necessity of external code having to
+    understand about other_info or manipulation of
+    update flags.
+    """
+    def __init__(self, nc_dnstr=None, ndr_blob=None):
+
+        self.__dict__['to_be_deleted'] = False
+        self.__dict__['nc_dnstr'] = nc_dnstr
+        self.__dict__['update_flags'] = 0x0
+
+        # WARNING:
+        #
+        # There is a very subtle bug here with python
+        # and our NDR code.  If you assign directly to
+        # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
+        # then a proper python GC reference count is not
+        # maintained.
+        #
+        # To work around this we maintain an internal
+        # reference to "dns_name(x)" and "other_info" elements
+        # of repsFromToBlob.  This internal reference
+        # is hidden within this class but it is why you
+        # see statements like this below:
+        #
+        #   self.__dict__['ndr_blob'].ctr.other_info = \
+        #        self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
+        #
+        # That would appear to be a redundant assignment but
+        # it is necessary to hold a proper python GC reference
+        # count.
+        if ndr_blob is None:
+            self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
+            self.__dict__['ndr_blob'].version = 0x1
+            self.__dict__['dns_name1'] = None
+            self.__dict__['dns_name2'] = None
+
+            self.__dict__['ndr_blob'].ctr.other_info = \
+                self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
+
+        else:
+            self.__dict__['ndr_blob'] = ndr_blob
+            self.__dict__['other_info'] = ndr_blob.ctr.other_info
+
+            if ndr_blob.version == 0x1:
+                self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
+                self.__dict__['dns_name2'] = None
+            else:
+                self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
+                self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
+
+    def __str__(self):
+        '''Debug dump string output of class'''
+
+        text = "%s:" % self.__class__.__name__
+        text = text + "\n\tdnstr=%s" % self.nc_dnstr
+        text = text + "\n\tupdate_flags=0x%X" % self.update_flags
+
+        text = text + "\n\tversion=%d" % self.version
+        text = text + "\n\tsource_dsa_obj_guid=%s" % \
+               str(self.source_dsa_obj_guid)
+        text = text + "\n\tsource_dsa_invocation_id=%s" % \
+               str(self.source_dsa_invocation_id)
+        text = text + "\n\ttransport_guid=%s" % \
+               str(self.transport_guid)
+        text = text + "\n\treplica_flags=0x%X" % \
+               self.replica_flags
+        text = text + "\n\tconsecutive_sync_failures=%d" % \
+               self.consecutive_sync_failures
+        text = text + "\n\tlast_success=%s" % \
+               self.last_success
+        text = text + "\n\tlast_attempt=%s" % \
+               self.last_attempt
+        text = text + "\n\tdns_name1=%s" % \
+               str(self.dns_name1)
+        text = text + "\n\tdns_name2=%s" % \
+               str(self.dns_name2)
+        text = text + "\n\tschedule[ "
+        for slot in self.schedule:
+            text = text + "0x%X " % slot
+        text = text + "]"
+
+        return text
+
+    def __setattr__(self, item, value):
+
+        if item in [ 'schedule', 'replica_flags', 'transport_guid',
+                     'source_dsa_obj_guid', 'source_dsa_invocation_id',
+                     'consecutive_sync_failures', 'last_success',
+                     'last_attempt' ]:
+
+            if item in ['replica_flags']:
+                self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
+            elif item in ['schedule']:
+                self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
+
+            setattr(self.__dict__['ndr_blob'].ctr, item, value)
+
+        elif item in ['dns_name1']:
+            self.__dict__['dns_name1'] = value
+
+            if self.__dict__['ndr_blob'].version == 0x1:
+                self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
+                    self.__dict__['dns_name1']
+            else:
+                self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
+                    self.__dict__['dns_name1']
+
+        elif item in ['dns_name2']:
+            self.__dict__['dns_name2'] = value
+
+            if self.__dict__['ndr_blob'].version == 0x1:
+                raise AttributeError(item)
+            else:
+                self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
+                    self.__dict__['dns_name2']
+
+        elif item in ['nc_dnstr']:
+            self.__dict__['nc_dnstr'] = value
+
+        elif item in ['to_be_deleted']:
+            self.__dict__['to_be_deleted'] = value
+
+        elif item in ['version']:
+            raise AttributeError, "Attempt to set readonly attribute %s" % item
+        else:
+            raise AttributeError, "Unknown attribute %s" % item
+
+        self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
+
+    def __getattr__(self, item):
+        """Overload of RepsFromTo attribute retrieval.
+
+        Allows external code to ignore substructures within the blob
+        """
+        if item in [ 'schedule', 'replica_flags', 'transport_guid',
+                     'source_dsa_obj_guid', 'source_dsa_invocation_id',
+                     'consecutive_sync_failures', 'last_success',
+                     'last_attempt' ]:
+            return getattr(self.__dict__['ndr_blob'].ctr, item)
+
+        elif item in ['version']:
+            return self.__dict__['ndr_blob'].version
+
+        elif item in ['dns_name1']:
+            if self.__dict__['ndr_blob'].version == 0x1:
+                return self.__dict__['ndr_blob'].ctr.other_info.dns_name
+            else:
+                return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
+
+        elif item in ['dns_name2']:
+            if self.__dict__['ndr_blob'].version == 0x1:
+                raise AttributeError(item)
+            else:
+                return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
+
+        elif item in ['to_be_deleted']:
+            return self.__dict__['to_be_deleted']
+
+        elif item in ['nc_dnstr']:
+            return self.__dict__['nc_dnstr']
+
+        elif item in ['update_flags']:
+            return self.__dict__['update_flags']
+
+        raise AttributeError, "Unknwown attribute %s" % item
+
+    def is_modified(self):
+        return (self.update_flags != 0x0)
+
+    def set_unmodified(self):
+        self.__dict__['update_flags'] = 0x0
+
+
+class SiteLink(object):
+    """Class defines a site link found under sites
+    """
+
+    def __init__(self, dnstr):
+        self.dnstr = dnstr
+        self.options = 0
+        self.system_flags = 0
+        self.cost = 0
+        self.schedule = None
+        self.interval = None
+        self.site_list = []
+
+    def __str__(self):
+        '''Debug dump string output of Transport object'''
+
+        text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
+        text = text + "\n\toptions=%d" % self.options
+        text = text + "\n\tsystem_flags=%d" % self.system_flags
+        text = text + "\n\tcost=%d" % self.cost
+        text = text + "\n\tinterval=%s" % self.interval
+
+        if self.schedule is not None:
+            text = text + "\n\tschedule.size=%s" % self.schedule.size
+            text = text + "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
+            text = text + "\n\tschedule.numberOfSchedules=%s" % \
+                   self.schedule.numberOfSchedules
+
+            for i, header in enumerate(self.schedule.headerArray):
+                text = text + "\n\tschedule.headerArray[%d].type=%d" % \
+                       (i, header.type)
+                text = text + "\n\tschedule.headerArray[%d].offset=%d" % \
+                       (i, header.offset)
+                text = text + "\n\tschedule.dataArray[%d].slots[ " % i
+                for slot in self.schedule.dataArray[i].slots:
+                    text = text + "0x%X " % slot
+                text = text + "]"
+
+        for dnstr in self.site_list:
+            text = text + "\n\tsite_list=%s" % dnstr
+        return text
+
+    def load_sitelink(self, samdb):
+        """Given a siteLink object with an prior initialization
+        for the object's DN, search for the DN and load attributes
+        from the samdb.
+        """
+        attrs = [ "options",
+                  "systemFlags",
+                  "cost",
+                  "schedule",
+                  "replInterval",
+                  "siteList" ]
+        try:
+            res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
+                               attrs=attrs)
+
+        except ldb.LdbError, (enum, estr):
+            raise Exception("Unable to find SiteLink for (%s) - (%s)" %
+                            (self.dnstr, estr))
+
+        msg = res[0]
+
+        if "options" in msg:
+            self.options = int(msg["options"][0])
+
+        if "systemFlags" in msg:
+            self.system_flags = int(msg["systemFlags"][0])
+
+        if "cost" in msg:
+            self.cost = int(msg["cost"][0])
+
+        if "replInterval" in msg:
+            self.interval = int(msg["replInterval"][0])
+
+        if "siteList" in msg:
+            for value in msg["siteList"]:
+                dsdn = dsdb_Dn(samdb, value)
+                dnstr = str(dsdn.dn)
+                if dnstr not in self.site_list:
+                    self.site_list.append(dnstr)
+
+    def is_sitelink(self, site1_dnstr, site2_dnstr):
+        """Given a siteLink object, determine if it is a link
+        between the two input site DNs
+        """
+        if site1_dnstr in self.site_list and site2_dnstr in self.site_list:
+            return True
+        return False
+
+
+class VertexColor(object):
+    (unknown, white, black, red) = range(0, 4)
+
+
+class Vertex(object):
+    """Class encapsulation of a Site Vertex in the
+    intersite topology replication algorithm
+    """
+    def __init__(self, site, part):
+        self.site = site
+        self.part = part
+        self.color = VertexColor.unknown
+
+    def color_vertex(self):
+        """Color each vertex to indicate which kind of NC
+        replica it contains
+        """
+        # IF s contains one or more DCs with full replicas of the
+        # NC cr!nCName
+        #    SET v.Color to COLOR.RED
+        # ELSEIF s contains one or more partial replicas of the NC
+        #    SET v.Color to COLOR.BLACK
+        #ELSE
+        #    SET v.Color to COLOR.WHITE
+
+        # set to minimum (no replica)
+        self.color = VertexColor.white
+
+        for dnstr, dsa in self.site.dsa_table.items():
+            rep = dsa.get_current_replica(self.part.nc_dnstr)
+            if rep is None:
+                continue
+
+            # We have a full replica which is the largest
+            # value so exit
+            if not rep.is_partial():
+                self.color = VertexColor.red
+                break
+            else:
+                self.color = VertexColor.black
+
+    def is_red(self):
+        assert(self.color != VertexColor.unknown)
+        return (self.color == VertexColor.red)
+
+    def is_black(self):
+        assert(self.color != VertexColor.unknown)
+        return (self.color == VertexColor.black)
+
+    def is_white(self):
+        assert(self.color != VertexColor.unknown)
+        return (self.color == VertexColor.white)
+
+##################################################
+# Global Functions
+##################################################
+def sort_dsa_by_guid(dsa1, dsa2):
+    return cmp(dsa1.dsa_guid, dsa2.dsa_guid)