summaryrefslogtreecommitdiff
path: root/source4/scripting
diff options
context:
space:
mode:
Diffstat (limited to 'source4/scripting')
-rwxr-xr-xsource4/scripting/bin/samba_kcc720
-rw-r--r--source4/scripting/python/samba/kcc_utils.py790
2 files changed, 1292 insertions, 218 deletions
diff --git a/source4/scripting/bin/samba_kcc b/source4/scripting/bin/samba_kcc
index c024cd41ef..c17439e637 100755
--- a/source4/scripting/bin/samba_kcc
+++ b/source4/scripting/bin/samba_kcc
@@ -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)
diff --git a/source4/scripting/python/samba/kcc_utils.py b/source4/scripting/python/samba/kcc_utils.py
index ac7449acd0..13bc2412d6 100644
--- a/source4/scripting/python/samba/kcc_utils.py
+++ b/source4/scripting/python/samba/kcc_utils.py
@@ -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)