samba_kcc NTDSConnection translation
authorDave Craft <wimberosa@gmail.com>
Sun, 4 Dec 2011 17:08:56 +0000 (11:08 -0600)
committerAndrew Tridgell <tridge@samba.org>
Thu, 8 Dec 2011 00:48:17 +0000 (11:48 +1100)
This is an advancement of samba_kcc to compute and
commit the modification of a repsFrom on an NC Replica.
The repsFrom is computed according to the MS tech spec
for implied replicas of NTDSConnections.  Proper maintenance
of (DRS options, schedules, etc) from a NTDSConnection are now
all present.  New classes for inter-site transports, sites,
and repsFrom) are now present in kcc_utils.py.  Substantively
this gets intra-site topology generation functional by committing
the repsFrom that were computed from the DSA graph implemented in
prior drops of samba_kcc

Signed-off-by: Andrew Tridgell <tridge@samba.org>
source4/scripting/bin/samba_kcc
source4/scripting/python/samba/kcc_utils.py

index c024cd41ef0f03db249174d9298b99ab58425fc4..c17439e63760bcd31bd3389b8544f21532e0e82f 100755 (executable)
@@ -20,6 +20,7 @@
 import os
 import sys
 import random
+import copy
 
 # ensure we get messages out immediately, so they get in the samba logs,
 # and don't get swallowed by a timeout
@@ -41,6 +42,7 @@ import logging
 from samba           import getopt as options
 from samba.auth      import system_session
 from samba.samdb     import SamDB
+from samba.dcerpc    import drsuapi
 from samba.kcc_utils import *
 
 class KCC:
@@ -55,12 +57,47 @@ class KCC:
            our local DCs partitions or all the partitions in
            the forest
         """
-        self.dsa_table     = {}    # dsa objects
-        self.part_table    = {}    # partition objects
-        self.site_table    = {}
+        self.part_table      = {}    # partition objects
+        self.site_table      = {}
+        self.transport_table = {}
+
         self.my_dsa_dnstr  = None  # My dsa DN
+        self.my_dsa        = None  # My dsa object
+
         self.my_site_dnstr = None
+        self.my_site       = None
+
         self.samdb         = samdb
+        return
+
+    def load_all_transports(self):
+        """Loads the inter-site transport objects for Sites
+           Raises an Exception on error
+        """
+        try:
+            res = samdb.search("CN=Inter-Site Transports,CN=Sites,%s" % \
+                               samdb.get_config_basedn(),
+                               scope=ldb.SCOPE_SUBTREE,
+                               expression="(objectClass=interSiteTransport)")
+        except ldb.LdbError, (enum, estr):
+            raise Exception("Unable to find inter-site transports - (%s)" % estr)
+
+        for msg in res:
+            dnstr = str(msg.dn)
+
+            # already loaded
+            if dnstr in self.transport_table.keys():
+                continue
+
+            transport = Transport(dnstr)
+
+            transport.load_transport(samdb)
+
+            # Assign this transport to table
+            # and index by dn
+            self.transport_table[dnstr] = transport
+
+        return
 
     def load_my_site(self):
         """Loads the Site class for the local DSA
@@ -69,14 +106,14 @@ class KCC:
         self.my_site_dnstr = "CN=%s,CN=Sites,%s" % (samdb.server_site_name(),
                              samdb.get_config_basedn())
         site = Site(self.my_site_dnstr)
-
         site.load_site(samdb)
+
         self.site_table[self.my_site_dnstr] = site
+        self.my_site = site
+        return
 
     def load_my_dsa(self):
-        """Discover my nTDSDSA thru the rootDSE entry and
-           instantiate and load the DSA.  The dsa is inserted
-           into the dsa_table by dn string
+        """Discover my nTDSDSA dn thru the rootDSE entry
            Raises an Exception on error.
         """
         dn = ldb.Dn(self.samdb, "")
@@ -86,49 +123,10 @@ class KCC:
         except ldb.LdbError, (enum, estr):
             raise Exception("Unable to find my nTDSDSA - (%s)" % estr)
 
-        dnstr = res[0]["dsServiceName"][0]
-
-        # already loaded
-        if dnstr in self.dsa_table.keys():
-            return
-
-        self.my_dsa_dnstr = dnstr
-        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 load_all_dsa(self):
-        """Discover all nTDSDSA thru the sites entry and
-           instantiate and load the DSAs.  Each dsa is inserted
-           into the dsa_table by dn string.
-           Raises an Exception on error.
-        """
-        try:
-            res = self.samdb.search("CN=Sites,%s" %
-                                    self.samdb.get_config_basedn(),
-                                    scope=ldb.SCOPE_SUBTREE,
-                                    expression="(objectClass=nTDSDSA)")
-        except ldb.LdbError, (enum, estr):
-            raise Exception("Unable to find nTDSDSAs - (%s)" % estr)
+        self.my_dsa_dnstr = res[0]["dsServiceName"][0]
+        self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
 
-        for msg in res:
-            dnstr = str(msg.dn)
-
-            # already loaded
-            if dnstr in self.dsa_table.keys():
-                continue
-
-            dsa = DirectoryServiceAgent(dnstr)
-
-            dsa.load_dsa(self.samdb)
-
-            # Assign this dsa to my dsa table
-            # and index by dsa dn
-            self.dsa_table[dnstr] = dsa
+        return
 
     def load_all_partitions(self):
         """Discover all NCs thru the Partitions dn and
@@ -158,16 +156,15 @@ class KCC:
             self.part_table[partstr] = part
 
     def should_be_present_test(self):
-        """Enumerate all loaded partitions and DSAs and test
-           if NC should be present as replica
+        """Enumerate all loaded partitions and DSAs in local
+           site and test if NC should be present as replica
         """
         for partdn, part in self.part_table.items():
-
-           for dsadn, dsa in self.dsa_table.items():
+            for dsadn, dsa in self.my_site.dsa_table.items():
                needed, ro, partial = part.should_be_present(dsa)
-
                logger.info("dsadn:%s\nncdn:%s\nneeded=%s:ro=%s:partial=%s\n" % \
-                           (dsa.dsa_dnstr, part.nc_dnstr, needed, ro, partial))
+                           (dsadn, part.nc_dnstr, needed, ro, partial))
+        return
 
     def refresh_failed_links_connections(self):
         # XXX - not implemented yet
@@ -186,12 +183,500 @@ class KCC:
         # XXX - not implemented yet
         return
 
-    def remove_unneeded_ntds_connections(self):
+    def remove_unneeded_ntdsconn(self):
         # XXX - not implemented yet
         return
 
-    def translate_connections(self):
-        # XXX - not implemented yet
+    def get_dsa_by_guidstr(self, guidstr):
+        """Given a DSA guid string, consule all sites looking
+           for the corresponding DSA and return it.
+        """
+        for site in self.site_table.values():
+            dsa = site.get_dsa_by_guidstr(guidstr)
+            if dsa is not None:
+                return dsa
+        return None
+
+    def get_dsa(self, dnstr):
+        """Given a DSA dn string, consule all sites looking
+           for the corresponding DSA and return it.
+        """
+        for site in self.site_table.values():
+            dsa = site.get_dsa(dnstr)
+            if dsa is not None:
+                return dsa
+        return None
+
+    def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
+        """Update t_repsFrom if necessary to satisfy requirements. Such
+           updates are typically required when the IDL_DRSGetNCChanges
+           server has moved from one site to another--for example, to
+           enable compression when the server is moved from the
+           client's site to another site.
+           :param n_rep: NC replica we need
+           :param t_repsFrom: repsFrom tuple to modify
+           :param s_rep: NC replica at source DSA
+           :param s_dsa: source DSA
+           :param cn_conn: Local DSA NTDSConnection child
+           Returns (update) bit field containing which portion of the
+           repsFrom was modified.  This bit field is suitable as input
+           to IDL_DRSReplicaModify ulModifyFields element, as it consists
+           of these bits:
+               drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
+               drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
+               drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
+        """
+        s_dnstr = s_dsa.dsa_dnstr
+        update  = 0x0
+
+        if self.my_site.get_dsa(s_dnstr) is s_dsa:
+            same_site = True
+        else:
+            same_site = False
+
+        times = cn_conn.convert_schedule_to_repltimes()
+
+        # if schedule doesn't match then update and modify
+        if times != t_repsFrom.schedule:
+            t_repsFrom.schedule = times
+
+        # Bit DRS_PER_SYNC is set in replicaFlags if and only
+        # if nTDSConnection schedule has a value v that specifies
+        # scheduled replication is to be performed at least once
+        # per week.
+        if cn_conn.is_schedule_minimum_once_per_week():
+
+            if (t_repsFrom.replica_flags & \
+                drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0:
+                t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
+
+        # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
+        # if the source DSA and the local DC's nTDSDSA object are
+        # in the same site or source dsa is the FSMO role owner
+        # of one or more FSMO roles in the NC replica.
+        if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
+
+            if (t_repsFrom.replica_flags & \
+                drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0:
+                t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
+
+        # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
+        # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
+        # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
+        # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
+        # t.replicaFlags if and only if s and the local DC's
+        # nTDSDSA object are in different sites.
+        if (cn_conn.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0:
+
+            if (cn_conn.option & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
+
+                if (t_repsFrom.replica_flags & \
+                    drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0:
+                    t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
+
+        elif same_site == False:
+
+            if (t_repsFrom.replica_flags & \
+                drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0:
+                t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
+
+        # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
+        # and only if s and the local DC's nTDSDSA object are
+        # not in the same site and the
+        # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
+        # clear in cn!options
+        if same_site == False and \
+           (cn_conn.options & \
+            dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0:
+
+            if (t_repsFrom.replica_flags & \
+                drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0:
+                t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
+
+        # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
+        # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
+        if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
+
+            if (t_repsFrom.replica_flags & \
+                drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0:
+                t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
+
+        # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
+        # set in t.replicaFlags if and only if cn!enabledConnection = false.
+        if cn_conn.is_enabled() == False:
+
+            if (t_repsFrom.replica_flags & \
+                drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0:
+                t_repsFrom.replica_flags |= \
+                    drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
+
+            if (t_repsFrom.replica_flags & \
+                drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0:
+                t_repsFrom.replica_flags |= \
+                    drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
+
+        # If s and the local DC's nTDSDSA object are in the same site,
+        # cn!transportType has no value, or the RDN of cn!transportType
+        # is CN=IP:
+        #
+        #     Bit DRS_MAIL_REP in t.replicaFlags is clear.
+        #
+        #     t.uuidTransport = NULL GUID.
+        #
+        #     t.uuidDsa = The GUID-based DNS name of s.
+        #
+        # Otherwise:
+        #
+        #     Bit DRS_MAIL_REP in t.replicaFlags is set.
+        #
+        #     If x is the object with dsname cn!transportType,
+        #     t.uuidTransport = x!objectGUID.
+        #
+        #     Let a be the attribute identified by
+        #     x!transportAddressAttribute. If a is
+        #     the dNSHostName attribute, t.uuidDsa = the GUID-based
+        #      DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
+        #
+        # It appears that the first statement i.e.
+        #
+        #     "If s and the local DC's nTDSDSA object are in the same
+        #      site, cn!transportType has no value, or the RDN of
+        #      cn!transportType is CN=IP:"
+        #
+        # could be a slightly tighter statement if it had an "or"
+        # between each condition.  I believe this should
+        # be interpreted as:
+        #
+        #     IF (same-site) OR (no-value) OR (type-ip)
+        #
+        # because IP should be the primary transport mechanism
+        # (even in inter-site) and the absense of the transportType
+        # attribute should always imply IP no matter if its multi-site
+        #
+        # NOTE MS-TECH INCORRECT:
+        #
+        #     All indications point to these statements above being
+        #     incorrectly stated:
+        #
+        #         t.uuidDsa = The GUID-based DNS name of s.
+        #
+        #         Let a be the attribute identified by
+        #         x!transportAddressAttribute. If a is
+        #         the dNSHostName attribute, t.uuidDsa = the GUID-based
+        #         DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
+        #
+        #     because the uuidDSA is a GUID and not a GUID-base DNS
+        #     name.  Nor can uuidDsa hold (s!parent)!a if not
+        #     dNSHostName.  What should have been said is:
+        #
+        #         t.naDsa = The GUID-based DNS name of s
+        #
+        #     That would also be correct if transportAddressAttribute
+        #     were "mailAddress" because (naDsa) can also correctly
+        #     hold the SMTP ISM service address.
+        #
+        nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
+
+        # We're not currently supporting SMTP replication
+        # so is_smtp_replication_available() is currently
+        # always returning False
+        if same_site == True or \
+           cn_conn.transport_dnstr == None or \
+           cn_conn.transport_dnstr.find("CN=IP") == 0 or \
+           is_smtp_replication_available() == False:
+
+            if (t_repsFrom.replica_flags & \
+                drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0:
+                t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
+
+            null_guid = misc.GUID()
+            if t_repsFrom.transport_guid is None or \
+               t_repsFrom.transport_guid != null_guid:
+                t_repsFrom.transport_guid = null_guid
+
+            # See (NOTE MS-TECH INCORRECT) above
+            if t_repsFrom.version == 0x1:
+                if t_repsFrom.dns_name1 is None or \
+                   t_repsFrom.dns_name1 != nastr:
+                    t_repsFrom.dns_name1 = nastr
+            else:
+                if t_repsFrom.dns_name1 is None or \
+                   t_repsFrom.dns_name2 is None or \
+                   t_repsFrom.dns_name1 != nastr or \
+                   t_repsFrom.dns_name2 != nastr:
+                    t_repsFrom.dns_name1 = nastr
+                    t_repsFrom.dns_name2 = nastr
+
+        else:
+            if (t_repsFrom.replica_flags & \
+                drsuapi.DRSUAPI_DRS_MAIL_REP) == 0x0:
+                t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_MAIL_REP
+
+            # We have a transport type but its not an
+            # object in the database
+            if cn_conn.transport_dnstr not in self.transport_table.keys():
+                raise Exception("Missing inter-site transport - (%s)" % \
+                                cn_conn.transport_dnstr)
+
+            x_transport = self.transport_table[cn_conn.transport_dnstr]
+
+            if t_repsFrom.transport_guid != x_transport.guid:
+                t_repsFrom.transport_guid = x_transport.guid
+
+            # See (NOTE MS-TECH INCORRECT) above
+            if x_transport.addr_attr == "dNSHostName":
+
+                if t_repsFrom.version == 0x1:
+                    if t_repsFrom.dns_name1 is None or \
+                       t_repsFrom.dns_name1 != nastr:
+                        t_repsFrom.dns_name1 = nastr
+                else:
+                    if t_repsFrom.dns_name1 is None or \
+                       t_repsFrom.dns_name2 is None or \
+                       t_repsFrom.dns_name1 != nastr or \
+                       t_repsFrom.dns_name2 != nastr:
+                        t_repsFrom.dns_name1 = nastr
+                        t_repsFrom.dns_name2 = nastr
+
+            else:
+                # MS tech specification says we retrieve the named
+                # attribute in "addr_attr" from the parent of the
+                # DSA object
+                try:
+                    pdnstr = s_dsa.get_parent_dnstr()
+                    attrs  = [ x_transport.addr_attr ]
+
+                    res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
+                                            attrs=attrs)
+                except ldb.ldbError, (enum, estr):
+                    raise Exception \
+                        ("Unable to find attr (%s) for (%s) - (%s)" % \
+                         (x_transport.addr_attr, pdnstr, estr))
+
+                msg = res[0]
+                nastr = str(msg[x_transport.addr_attr][0])
+
+                # See (NOTE MS-TECH INCORRECT) above
+                if t_repsFrom.version == 0x1:
+                    if t_repsFrom.dns_name1 is None or \
+                       t_repsFrom.dns_name1 != nastr:
+                        t_repsFrom.dns_name1 = nastr
+                else:
+                    if t_repsFrom.dns_name1 is None or \
+                       t_repsFrom.dns_name2 is None or \
+                       t_repsFrom.dns_name1 != nastr or \
+                       t_repsFrom.dns_name2 != nastr:
+
+                        t_repsFrom.dns_name1 = nastr
+                        t_repsFrom.dns_name2 = nastr
+
+        if t_repsFrom.is_modified():
+            logger.debug("modify_repsFrom(): %s" % t_repsFrom)
+        return
+
+    def translate_ntdsconn(self):
+        """This function adjusts values of repsFrom abstract attributes of NC
+           replicas on the local DC to match those implied by
+           nTDSConnection objects.
+        """
+        logger.debug("translate_ntdsconn(): enter mydsa:\n%s" % self.my_dsa)
+
+        if self.my_dsa.should_translate_ntdsconn() == False:
+            return
+
+        current_rep_table, needed_rep_table = self.my_dsa.get_rep_tables()
+
+        # Filled in with replicas we currently have that need deleting
+        delete_rep_table = {}
+
+        # Table of replicas needed, combined with our local information
+        # if we already have the replica.  This may be a superset list of
+        # replicas if we need additional NC replicas that we currently
+        # don't have local copies for
+        translate_rep_table = {}
+
+        # We're using the MS notation names here to allow
+        # correlation back to the published algorithm.
+        #
+        # n_rep      - NC replica (n)
+        # t_repsFrom - tuple (t) in n!repsFrom
+        # s_dsa      - Source DSA of the replica. Defined as nTDSDSA
+        #              object (s) such that (s!objectGUID = t.uuidDsa)
+        #              In our IDL representation of repsFrom the (uuidDsa)
+        #              attribute is called (source_dsa_obj_guid)
+        # cn_conn    - (cn) is nTDSConnection object and child of the local DC's
+        #              nTDSDSA object and (cn!fromServer = s)
+        # s_rep      - source DSA replica of n
+        #
+        # Build a list of replicas that we will run translation
+        # against.  If we have the replica and its not needed
+        # then we add it to the "to be deleted" list.  Otherwise
+        # we have it and we need it so move it to the translate list
+        for dnstr, n_rep in current_rep_table.items():
+            if dnstr not in needed_rep_table.keys():
+                delete_rep_table[dnstr] = n_rep
+            else:
+                translate_rep_table[dnstr] = n_rep
+
+        # If we need the replica yet we don't have it (not in
+        # translate list) then add it
+        for dnstr, n_rep in needed_rep_table.items():
+            if dnstr not in translate_rep_table.keys():
+                translate_rep_table[dnstr] = n_rep
+
+        # Now perform the scan of replicas we'll need
+        # and compare any current repsFrom against the
+        # connections
+        for dnstr, n_rep in translate_rep_table.items():
+
+            # load any repsFrom and fsmo roles as we'll
+            # need them during connection translation
+            n_rep.load_repsFrom(self.samdb)
+            n_rep.load_fsmo_roles(self.samdb)
+
+            # Loop thru the existing repsFrom tupples (if any)
+            for i, t_repsFrom in enumerate(n_rep.rep_repsFrom):
+
+                # for each tuple t in n!repsFrom, let s be the nTDSDSA
+                # object such that s!objectGUID = t.uuidDsa
+                guidstr = str(t_repsFrom.source_dsa_obj_guid)
+                s_dsa = self.get_dsa_by_guidstr(guidstr)
+
+                # Source dsa is gone from config (strange)
+                # so cleanup stale repsFrom for unlisted DSA
+                if s_dsa is None:
+                    logger.debug("repsFrom source DSA guid (%s) not found" % \
+                                 guidstr)
+                    t_repsFrom.to_be_deleted = True
+                    continue
+
+                s_dnstr = s_dsa.dsa_dnstr
+
+                # Retrieve my DSAs connection object (if it exists)
+                # that specifies the fromServer equivalent to
+                # the DSA that is specified in the repsFrom source
+                cn_conn = self.my_dsa.get_connection_by_from_dnstr(s_dnstr)
+
+                # Let (cn) be the nTDSConnection object such that (cn)
+                # is a child of the local DC's nTDSDSA object and
+                # (cn!fromServer = s) and (cn!options) does not contain
+                # NTDSCONN_OPT_RODC_TOPOLOGY or NULL if no such (cn) exists.
+                if cn_conn and cn_conn.is_rodc_topology() == True:
+                    cn_conn = None
+
+                # KCC removes this repsFrom tuple if any of the following
+                # is true:
+                #     cn = NULL.
+                #
+                #     No NC replica of the NC "is present" on DSA that
+                #     would be source of replica
+                #
+                #     A writable replica of the NC "should be present" on
+                #     the local DC, but a partial replica "is present" on
+                #     the source DSA
+                s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
+
+                if cn_conn is None or \
+                   s_rep is None or s_rep.is_present() == False or \
+                   (n_rep.is_ro() == False and s_rep.is_partial() == True):
+
+                    t_repsFrom.to_be_deleted = True
+                    continue
+
+                # If the KCC did not remove t from n!repsFrom, it updates t
+                self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
+
+            # Loop thru connections and add implied repsFrom tuples
+            # for each NTDSConnection under our local DSA if the
+            # repsFrom is not already present
+            for cn_dnstr, cn_conn in self.my_dsa.connect_table.items():
+
+                # NTDS Connection must satisfy all the following criteria
+                # to imply a repsFrom tuple is needed:
+                #
+                #    cn!enabledConnection = true.
+                #    cn!options does not contain NTDSCONN_OPT_RODC_TOPOLOGY.
+                #    cn!fromServer references an nTDSDSA object.
+                s_dsa = None
+
+                if cn_conn.is_enabled() == True and \
+                   cn_conn.is_rodc_topology() == False:
+
+                    s_dnstr = cn_conn.get_from_dnstr()
+                    if s_dnstr is not None:
+                        s_dsa = self.get_dsa(s_dnstr)
+
+                if s_dsa == None:
+                    continue
+
+                # Loop thru the existing repsFrom tupples (if any) and
+                # if we already have a tuple for this connection then
+                # no need to proceed to add.  It will have been changed
+                # to have the correct attributes above
+                for i, t_repsFrom in enumerate(n_rep.rep_repsFrom):
+
+                     guidstr = str(t_repsFrom.source_dsa_obj_guid)
+                     if s_dsa is self.get_dsa_by_guidstr(guidstr):
+                         s_dsa = None
+                         break
+
+                if s_dsa == None:
+                    continue
+
+                # Source dsa is gone from config (strange)
+                # To imply a repsFrom tuple is needed, each of these
+                # must be True:
+                #
+                #     An NC replica of the NC "is present" on the DC to
+                #     which the nTDSDSA object referenced by cn!fromServer
+                #     corresponds.
+                #
+                #     An NC replica of the NC "should be present" on
+                #     the local DC
+                s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
+
+                if s_rep is None or s_rep.is_present() == False:
+                    continue
+
+                # To imply a repsFrom tuple is needed, each of these
+                # must be True:
+                #
+                #     The NC replica on the DC referenced by cn!fromServer is
+                #     a writable replica or the NC replica that "should be
+                #     present" on the local DC is a partial replica.
+                #
+                #     The NC is not a domain NC, the NC replica that
+                #     "should be present" on the local DC is a partial
+                #     replica, cn!transportType has no value, or
+                #     cn!transportType has an RDN of CN=IP.
+                #
+                implies = (s_rep.is_ro() == False or \
+                           n_rep.is_partial() == True) \
+                          and \
+                          (n_rep.is_domain() == False or\
+                           n_rep.is_partial() == True or \
+                           cn_conn.transport_dnstr == None or \
+                           cn_conn.transport_dnstr.find("CN=IP") == 0)
+
+                if implies == False:
+                    continue
+
+                # Create a new RepsFromTo and proceed to modify
+                # it according to specification
+                t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
+
+                t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
+
+                self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
+
+                # Add to our NC repsFrom as this is newly computed
+                if t_repsFrom.is_modified():
+                    n_rep.rep_repsFrom.append(t_repsFrom)
+
+            # Commit any modified repsFrom to the NC replica
+            if opts.readonly is None:
+                n_rep.commit_repsFrom(self.samdb)
+
         return
 
     def intersite(self):
@@ -260,11 +745,13 @@ class KCC:
         # partition (NC x) then continue
         needed, ro, partial = nc_x.should_be_present(dc_local)
 
-        logger.debug("construct_intrasite_graph:\n" + \
-                     "nc_x: %s\ndc_local: %s\n" % \
-                     (nc_x, dc_local) + \
-                     "gc_only: %s\nneeded: %s\nro: %s\npartial: %s" % \
-                     (gc_only, needed, ro, partial))
+        logger.debug("construct_intrasite_graph(): enter" + \
+                     "\n\tgc_only=%d" % gc_only + \
+                     "\n\tdetect_stale=%d" % detect_stale + \
+                     "\n\tneeded=%s" % needed + \
+                     "\n\tro=%s" % ro + \
+                     "\n\tpartial=%s" % partial + \
+                     "\n%s" % nc_x)
 
         if needed == False:
             return
@@ -279,6 +766,10 @@ class KCC:
         l_of_x.rep_partial  = partial
         l_of_x.rep_ro       = ro
 
+        # Add this replica that "should be present" to the
+        # needed replica table for this DSA
+        dc_local.add_needed_replica(l_of_x)
+
         # Empty replica sequence list
         r_list = []
 
@@ -286,16 +777,16 @@ class KCC:
         # writeable NC replicas that match the naming
         # context dn for (nc_x)
         #
-        for dc_s_dn, dc_s in self.dsa_table.items():
+        for dc_s_dn, dc_s in self.my_site.dsa_table.items():
 
             # If this partition (nc_x) doesn't appear as a
             # replica (f_of_x) on (dc_s) then continue
-            if not nc_x.nc_dnstr in dc_s.rep_table.keys():
+            if not nc_x.nc_dnstr in dc_s.current_rep_table.keys():
                 continue
 
             # Pull out the NCReplica (f) of (x) with the dn
             # that matches NC (x) we are examining.
-            f_of_x = dc_s.rep_table[nc_x.nc_dnstr]
+            f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
 
             # Replica (f) of NC (x) must be writable
             if f_of_x.is_ro() == True:
@@ -320,10 +811,9 @@ class KCC:
                 continue
 
             # DC (s) must be in the same site as the local DC
-            # This is the intra-site algorithm.  We are not
-            # replicating across multiple sites
-            if site_local.is_same_site(dc_s) == False:
-                continue
+            # as this is the intra-site algorithm. This is
+            # handled by virtue of placing DSAs in per
+            # site objects (see enclosing for() loop)
 
             # If NC (x) is intended to be read-only full replica
             # for a domain NC on the target DC then the source
@@ -361,17 +851,17 @@ class KCC:
             # Now we loop thru all the DSAs looking for
             # partial NC replicas that match the naming
             # context dn for (NC x)
-            for dc_s_dn, dc_s in self.dsa_table.items():
+            for dc_s_dn, dc_s in self.my_site.dsa_table.items():
 
                 # If this partition NC (x) doesn't appear as a
                 # replica (p) of NC (x) on the dsa DC (s) then
                 # continue
-                if not nc_x.nc_dnstr in dc_s.rep_table.keys():
+                if not nc_x.nc_dnstr in dc_s.current_rep_table.keys():
                     continue
 
                 # Pull out the NCReplica with the dn that
                 # matches NC (x) we are examining.
-                p_of_x = dsa.rep_table[nc_x.nc_dnstr]
+                p_of_x = dsa.current_rep_table[nc_x.nc_dnstr]
 
                 # Replica (p) of NC (x) must be partial
                 if p_of_x.is_partial() == False:
@@ -396,10 +886,9 @@ class KCC:
                     continue
 
                 # DC (s) must be in the same site as the local DC
-                # This is the intra-site algorithm.  We are not
-                # replicating across multiple sites
-                if site_local.is_same_site(dc_s) == False:
-                    continue
+                # as this is the intra-site algorithm. This is
+                # handled by virtue of placing DSAs in per
+                # site objects (see enclosing for() loop)
 
                 # This criteria is moot (a no-op) for this case
                 # because we are scanning for (partial = True).  The
@@ -476,7 +965,7 @@ class KCC:
         # to ri is less than n+2, the KCC adds that edge to the graph.
         i = 0
         while i < r_len:
-            dsa = self.dsa_table[graph_list[i].dsa_dnstr]
+            dsa = self.my_site.dsa_table[graph_list[i].dsa_dnstr]
             graph_list[i].add_edges_from_connections(dsa)
             i = i + 1
 
@@ -533,9 +1022,9 @@ class KCC:
            in the samdb
         """
         # Retrieve my DSA
-        mydsa = self.dsa_table[self.my_dsa_dnstr]
+        mydsa = self.my_dsa
 
-        logger.debug("intrasite enter:\nmydsa: %s" % mydsa)
+        logger.debug("intrasite(): enter mydsa:\n%s" % mydsa)
 
         # Test whether local site has topology disabled
         mysite = self.site_table[self.my_site_dnstr]
@@ -584,52 +1073,51 @@ class KCC:
                                                False)  # don't detect stale
 
         # Commit any newly created connections to the samdb
-        mydsa.commit_connection_table(self.samdb)
-
-        logger.debug("intrasite exit:\nmydsa: %s" % mydsa)
+        if opts.readonly is None:
+            mydsa.commit_connection_table(self.samdb)
 
     def run(self):
         """Method to perform a complete run of the KCC and
            produce an updated topology for subsequent NC replica
            syncronization between domain controllers
         """
-        # Setup
         try:
-            self.load_my_dsa()
-            self.load_all_dsa()
-            self.load_all_partitions()
+            # Setup
             self.load_my_site()
+            self.load_my_dsa()
 
-        except Exception, estr:
-            logger.error("%s" % estr)
-            return
+            self.load_all_partitions()
+            self.load_all_transports()
 
-        # self.should_be_present_test()
+            # These are the published steps (in order) for the
+            # MS-TECH description of the KCC algorithm
 
-        # These are the published steps (in order) for the
-        # MS description of the KCC algorithm
+            # Step 1
+            self.refresh_failed_links_connections()
 
-        # Step 1
-        self.refresh_failed_links_connections()
+            # Step 2
+            self.intrasite()
 
-        # Step 2
-        self.intrasite()
+            # Step 3
+            self.intersite()
 
-        # Step 3
-        self.intersite()
+            # Step 4
+            self.remove_unneeded_ntdsconn()
 
-        # Step 4
-        self.remove_unneeded_ntds_connections()
+            # Step 5
+            self.translate_ntdsconn()
 
-        # Step 5
-        self.translate_connections()
+            # Step 6
+            self.remove_unneeded_failed_links_connections()
 
-        # Step 6
-        self.remove_unneeded_failed_links_connections()
+            # Step 7
+            self.update_rodc_connection()
 
-        # Step 7
-        self.update_rodc_connection()
+        except Exception, estr:
+            logger.error("%s" % estr)
+            return 1
 
+        return 0
 
 ##################################################
 # Global Functions
@@ -637,6 +1125,13 @@ class KCC:
 def sort_replica_by_dsa_guid(rep1, rep2):
     return cmp(rep1.rep_dsa_guid, rep2.rep_dsa_guid)
 
+def is_smtp_replication_availalbe():
+    """Currently always returns false because Samba
+       doesn't implement SMTP transfer for NC changes
+       between DCs
+    """
+    return False
+
 ##################################################
 # samba_kcc entry point
 ##################################################
@@ -649,8 +1144,11 @@ parser.add_option_group(sambaopts)
 parser.add_option_group(credopts)
 parser.add_option_group(options.VersionOptions(parser))
 
-parser.add_option("--debug", help="debug output", action="store_true")
-parser.add_option("--seed",  help="random number seed")
+parser.add_option("--readonly", \
+                  help="compute topology but do not update database", \
+                  action="store_true")
+parser.add_option("--debug",    help="debug output", action="store_true")
+parser.add_option("--seed",     help="random number seed")
 
 logger = logging.getLogger("samba_kcc")
 logger.addHandler(logging.StreamHandler(sys.stdout))
@@ -683,4 +1181,6 @@ except ldb.LdbError, (num, msg):
 
 # Instantiate Knowledge Consistency Checker and perform run
 kcc = KCC(samdb)
-kcc.run()
+rc  = kcc.run()
+
+sys.exit(rc)
index ac7449acd02f22d140677f023aaecab487795269..13bc2412d63089e3f5904c7e7978c73423454b83 100644 (file)
@@ -22,7 +22,11 @@ import uuid
 
 from samba              import dsdb
 from samba.dcerpc       import misc
+from samba.dcerpc       import drsblobs
+from samba.dcerpc       import drsuapi
 from samba.common       import dsdb_Dn
+from samba.ndr          import ndr_unpack
+from samba.ndr          import ndr_pack
 
 class NCType:
     (unknown, schema, domain, config, application) = range(0, 5)
@@ -36,20 +40,24 @@ class NamingContext:
     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_guid: NC guid
             :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
+        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)
+        text = "%s:" % self.__class__.__name__
+        text = text + "\n\tnc_dnstr=%s" % self.nc_dnstr
+        text = text + "\n\tnc_guid=%s"  % str(self.nc_guid)
+        text = text + "\n\tnc_sid=%s"   % self.nc_sid
+        text = text + "\n\tnc_type=%s"  % self.nc_type
+
+        return text
 
     def is_schema(self):
         '''Return True if NC is schema'''
@@ -79,7 +87,7 @@ class NamingContext:
             self.nc_type = NCType.schema
         elif self.nc_dnstr == str(samdb.get_config_basedn()):
             self.nc_type = NCType.config
-        elif self.nc_sid != None:
+        elif self.nc_sid is not None:
             self.nc_type = NCType.domain
         else:
             self.nc_type = NCType.application
@@ -118,7 +126,6 @@ class NamingContext:
 
         return
 
-
 class NCReplica(NamingContext):
     """Class defines a naming context replica that is relative
        to a specific DSA.  This is a more specific form of
@@ -131,15 +138,18 @@ class NCReplica(NamingContext):
         """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_guid: NC guid
             :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
+        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
+
+        # RepsFromTo tuples
+        self.rep_repsFrom = []
 
         # The (is present) test is a combination of being
         # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or
@@ -154,19 +164,25 @@ class NCReplica(NamingContext):
 
     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'''
+        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()
+
+        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 == None):
-            self.rep_flags = 0
+            self.rep_instantiated_flags = 0
         else:
-            self.rep_flags = flags
+            self.rep_instantiated_flags = flags
         return
 
     def identify_by_dsa_attr(self, samdb, attr):
@@ -239,10 +255,90 @@ class NCReplica(NamingContext):
            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:
+           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))
+            return
+
+        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)
+        return
+
+    def commit_repsFrom(self, samdb):
+        """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 = []
+
+        for repsFrom in self.rep_repsFrom:
+
+            # Leave out any to be deleted from
+            # replacement list
+            if repsFrom.to_be_deleted == True:
+                modify = True
+                continue
+
+            if repsFrom.is_modified():
+                modify = True
+
+            newreps.append(ndr_pack(repsFrom.ndr_blob))
+
+        # Nothing to do if no reps have been modified or
+        # need to be deleted.  Leave database record "as is"
+        if modify == False:
+            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))
+        return
+
+    def load_fsmo_roles(self, samdb):
+        #  XXX - to be implemented
+        return
+
+    def is_fsmo_role_owner(self, dsa_dnstr):
+        #  XXX - to be implemented
+        return False
 
 class DirectoryServiceAgent:
 
@@ -255,33 +351,50 @@ class DirectoryServiceAgent:
         self.dsa_guid      = None
         self.dsa_ivid      = None
         self.dsa_is_ro     = False
-        self.dsa_is_gc     = False
+        self.dsa_options   = 0
         self.dsa_behavior  = 0
         self.default_dnstr = None  # default domain dn string for dsa
 
-        # NCReplicas for this dsa.
+        # NCReplicas for this dsa that are "present"
         # Indexed by DN string of naming context
-        self.rep_table     = {}
+        self.current_rep_table = {}
 
-        # NTDSConnections for this dsa.
-        # Indexed by DN string of connection
+        # 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 "to be committed"
+        # in the database.  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())
+
+        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 + "\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):
+        return self.current_rep_table[nc_dnstr]
 
     def is_ro(self):
         '''Returns True if dsa a read only domain controller'''
@@ -289,7 +402,9 @@ class DirectoryServiceAgent:
 
     def is_gc(self):
         '''Returns True if dsa hosts a global catalog'''
-        return self.dsa_is_gc
+        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
@@ -301,6 +416,27 @@ class DirectoryServiceAgent:
             return True
         return False
 
+    def should_translate_ntdsconn(self):
+        """Returns True if DSA object allows NTDSConnection
+           translation in its options.  False otherwise.
+       """
+        if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0:
+            return False
+        return True
+
+    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):
+        """Drop the leading portion of the DN string
+           (e.g. CN=NTDS Settings,) which will give us
+           the parent DN string of this object
+        """
+        head, sep, tail = self.dsa_dnstr.partition(',')
+        return tail
+
     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
@@ -324,7 +460,7 @@ class DirectoryServiceAgent:
             return
 
         msg = res[0]
-        self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID",
+        self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID", \
                                   msg["objectGUID"][0]))
 
         # RODCs don't originate changes and thus have no invocationId,
@@ -333,11 +469,8 @@ class DirectoryServiceAgent:
             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 "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
@@ -348,7 +481,7 @@ class DirectoryServiceAgent:
             self.dsa_behavior = int(msg['msDS-Behavior-Version'][0])
 
         # Load the NC replicas that are enumerated on this dsa
-        self.load_replica_table(samdb)
+        self.load_current_replica_table(samdb)
 
         # Load the nTDSConnection that are enumerated on this dsa
         self.load_connection_table(samdb)
@@ -356,7 +489,7 @@ class DirectoryServiceAgent:
         return
 
 
-    def load_replica_table(self, 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,
@@ -373,7 +506,7 @@ class DirectoryServiceAgent:
                     # not RODC - default, config, schema, app NCs
                     "msDS-hasMasterNCs",
                     # domain NC partial replicas
-                    "hasPartialReplicANCs",
+                    "hasPartialReplicaNCs",
                     # default domain NC
                     "msDS-HasDomainNCs",
                     # RODC only - default, config, schema, app NCs
@@ -423,17 +556,17 @@ class DirectoryServiceAgent:
                         raise Exception("Missing GUID for (%s) - (%s: %s)" % \
                                         (self.dsa_dnstr, k, value))
                     else:
-                        guidstr = str(misc.GUID(guid))
+                        guid = misc.GUID(guid)
 
                     if not dnstr in tmp_table:
                         rep = NCReplica(self.dsa_dnstr, self.dsa_guid,
-                                        dnstr, guidstr, sid)
+                                        dnstr, guid, sid)
                         tmp_table[dnstr] = rep
                     else:
                         rep = tmp_table[dnstr]
 
                     if k == "msDS-HasInstantiatedNCs":
-                        rep.set_replica_flags(flags)
+                        rep.set_instantiated_flags(flags)
                         continue
 
                     rep.identify_by_dsa_attr(samdb, k)
@@ -447,7 +580,16 @@ class DirectoryServiceAgent:
             return
 
         # Assign our newly built NC replica table to this dsa
-        self.rep_table = tmp_table
+        self.current_rep_table = tmp_table
+        return
+
+    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
+
         return
 
     def load_connection_table(self, samdb):
@@ -480,14 +622,14 @@ class DirectoryServiceAgent:
 
     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)
+           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
         """
         for dnstr, connect in self.connect_table.items():
             connect.commit_connection(samdb)
 
-    def add_connection_by_dnstr(self, dnstr, connect):
+    def add_connection(self, dnstr, connect):
         self.connect_table[dnstr] = connect
         return
 
@@ -502,14 +644,24 @@ class DirectoryServiceAgent:
                 return connect
         return None
 
-    def dumpstr_replica_table(self):
-        '''Debug dump string output of replica table'''
+    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.rep_table.keys():
+        for k in self.needed_rep_table.keys():
             if text:
-                text = text + "\n%s" % self.rep_table[k]
+                text = text + "\n%s" % self.needed_rep_table[k]
             else:
-                text = "%s" % self.rep_table[k]
+                text = "%s" % self.needed_rep_table[k]
         return text
 
     def dumpstr_connect_table(self):
@@ -526,23 +678,50 @@ 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
+        self.dnstr           = dnstr
+        self.enabled         = False
+        self.committed       = False # new connection needs to be committed
+        self.options         = 0
+        self.flags           = 0
+        self.transport_dnstr = None
+        self.transport_guid  = None
+        self.from_dnstr      = None
+        self.from_guid       = None
+        self.schedule        = 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
+
+        text = "%s:\n\tdn=%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\ttransport_dn=%s" % self.transport_dnstr
+
+        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
+        text = text + "\n\tfrom_guid=%s" % str(self.from_guid)
+
+        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):
@@ -551,14 +730,16 @@ class NTDSConnection():
            from the samdb.
            Raises an Exception on error.
         """
+        controls = ["extended_dn:1:1"]
         attrs = [ "options",
                   "enabledConnection",
                   "schedule",
+                  "transportType",
                   "fromServer",
                   "systemFlags" ]
         try:
             res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
-                               attrs=attrs)
+                               attrs=attrs, controls=controls)
 
         except ldb.LdbError, (enum, estr):
             raise Exception("Unable to find nTDSConnection for (%s) - (%s)" % \
@@ -574,14 +755,30 @@ class NTDSConnection():
                 self.enabled = True
         if "systemFlags" in msg:
             self.flags = int(msg["systemFlags"][0])
+        if "transportType" in msg:
+            dsdn = dsdb_Dn(samdb, msg["tranportType"][0])
+            guid = dsdn.dn.get_extended_component('GUID')
+
+            assert guid is not None
+            self.transport_guid = misc.GUID(guid)
+
+            self.transport_dnstr = str(dsdn.dn)
+            assert self.transport_dnstr is not None
+
         if "schedule" in msg:
-            self.schedulestr = msg["schedule"][0]
+            self.schedule = ndr_unpack(drsblobs.replSchedule, msg["schedule"][0])
+
         if "fromServer" in msg:
             dsdn = dsdb_Dn(samdb, msg["fromServer"][0])
+            guid = dsdn.dn.get_extended_component('GUID')
+
+            assert guid is not None
+            self.from_guid = misc.GUID(guid)
+
             self.from_dnstr = str(dsdn.dn)
-            assert self.from_dnstr != None
+            assert self.from_dnstr is not None
 
-        # Appears as committed in the database
+        # Was loaded from database so connection is currently committed
         self.committed = True
         return
 
@@ -589,12 +786,109 @@ class NTDSConnection():
         """Given a NTDSConnection object that is not committed in the
            sam database, perform a commit action.
         """
-        if self.committed: # nothing to do
+        # nothing to do
+        if self.committed == True:
             return
 
-        # XXX - not yet written
+        # First verify we don't 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 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.flags), ldb.FLAG_MOD_ADD, "systemFlags")
+
+        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))
+        self.committed = True
         return
 
+    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 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_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
@@ -659,11 +953,11 @@ class Partition(NamingContext):
                     raise Exception("Missing GUID for (%s) - (%s: %s)" % \
                                     (self.partstr, k, value))
                 else:
-                    guidstr = str(misc.GUID(guid))
+                    guid = misc.GUID(guid)
 
                 if k == "nCName":
                     self.nc_dnstr = str(dsdn.dn)
-                    self.nc_guid  = guidstr
+                    self.nc_guid  = guid
                     self.nc_sid   = sid
                     continue
 
@@ -744,6 +1038,7 @@ class Site:
     def __init__(self, site_dnstr):
         self.site_dnstr   = site_dnstr
         self.site_options = 0
+        self.dsa_table    = {}
         return
 
     def load_site(self, samdb):
@@ -762,13 +1057,53 @@ class Site:
         msg = res[0]
         if "options" in msg:
             self.site_options = int(msg["options"][0])
+
+        self.load_all_dsa(samdb)
         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 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.
+           Raises an Exception on error.
+        """
+        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
+        return
+
+    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
+           Returns None if DSA doesn't exist
+        """
+        if dnstr in self.dsa_table.keys():
+            return self.dsa_table[dnstr]
+        return None
 
     def is_intrasite_topology_disabled(self):
         '''Returns True if intrasite topology is disabled for site'''
@@ -784,6 +1119,15 @@ class Site:
             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
+        for key, dsa in self.dsa_table.items():
+            text = text + "\n%s" % dsa
+        return text
+
 
 class GraphNode:
     """This is a graph node describing a set of edges that should be
@@ -801,16 +1145,19 @@ class GraphNode:
         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
+        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 != None
+        assert from_dsa_dnstr is not None
 
         # No edges from myself to myself
         if from_dsa_dnstr == self.dsa_dnstr:
@@ -855,8 +1202,7 @@ class GraphNode:
             #    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:
+            if connect and connect.is_rodc_topology() == False:
                 exists = True
             else:
                 exists = False
@@ -870,16 +1216,38 @@ class GraphNode:
             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
+            connect.committed   = False
+            connect.enabled     = True
+            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
+
+            # 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
 
-            # XXX I need to write the schedule blob
+            header = drsblobs.scheduleHeader()
+            header.type = 0
+            header.offset = 20
 
-            dsa.add_connection_by_dnstr(dnstr, connect);
+            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 ]
+
+            dsa.add_connection(dnstr, connect);
 
         return
 
@@ -888,3 +1256,209 @@ class GraphNode:
         if len(self.edge_from) >= self.max_edges:
             return True
         return False
+
+class Transport():
+    """Class defines a Inter-site transport found under Sites
+    """
+    def __init__(self, dnstr):
+        self.dnstr           = dnstr
+        self.options         = 0
+        self.guid            = None
+        self.address_attr    = None
+        return
+
+    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
+
+        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.
+           Raises an Exception on error.
+        """
+        attrs = [ "objectGUID",
+                  "options",
+                  "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))
+            return
+
+        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])
+
+        return
+
+class RepsFromTo:
+    """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
+        return
+
+    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' ]:
+            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 ['version']:
+            raise AttributeError, "Attempt to set readonly attribute %s" % item
+        else:
+            raise AttributeError, "Unknown attribute %s" % item
+
+        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
+        else:
+            self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
+
+        return
+
+    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
+
+        raise AttributeError, "Unknwown attribute %s" % item
+
+    def is_modified(self):
+        return (self.update_flags != 0x0)