summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xsource4/scripting/bin/samba_kcc1629
-rw-r--r--source4/scripting/python/samba/kcc_utils.py1176
2 files changed, 2429 insertions, 376 deletions
diff --git a/source4/scripting/bin/samba_kcc b/source4/scripting/bin/samba_kcc
index c17439e637..583d88f597 100755
--- a/source4/scripting/bin/samba_kcc
+++ b/source4/scripting/bin/samba_kcc
@@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
+import tempfile
import sys
import random
import copy
@@ -35,11 +36,15 @@ os.environ["TZ"] = "GMT"
# Find right directory when running from source tree
sys.path.insert(0, "bin/python")
-import samba, ldb
import optparse
import logging
-from samba import getopt as options
+from samba import (getopt as options, \
+ Ldb, \
+ ldb, \
+ dsdb, \
+ param, \
+ read_and_sub_file)
from samba.auth import system_session
from samba.samdb import SamDB
from samba.dcerpc import drsuapi
@@ -47,19 +52,25 @@ from samba.kcc_utils import *
class KCC:
"""The Knowledge Consistency Checker class. A container for
- objects and methods allowing a run of the KCC. Produces
- a set of connections in the samdb for which the Distributed
- Replication Service can then utilize to replicate naming
- contexts
+ objects and methods allowing a run of the KCC. Produces
+ a set of connections in the samdb for which the Distributed
+ Replication Service can then utilize to replicate naming
+ contexts
"""
- def __init__(self, samdb):
+ def __init__(self):
"""Initializes the partitions class which can hold
- our local DCs partitions or all the partitions in
- the forest
+ our local DCs partitions or all the partitions in
+ the forest
"""
self.part_table = {} # partition objects
self.site_table = {}
self.transport_table = {}
+ self.sitelink_table = {}
+
+ # Used in inter-site topology computation. A list
+ # of connections (by NTDSConnection object) that are
+ # to be kept when pruning un-needed NTDS Connections
+ self.keep_connection_list = []
self.my_dsa_dnstr = None # My dsa DN
self.my_dsa = None # My dsa object
@@ -67,18 +78,19 @@ class KCC:
self.my_site_dnstr = None
self.my_site = None
- self.samdb = samdb
+ self.samdb = None
return
def load_all_transports(self):
"""Loads the inter-site transport objects for Sites
- Raises an Exception on error
+
+ ::returns: 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)")
+ res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" % \
+ self.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)
@@ -91,7 +103,7 @@ class KCC:
transport = Transport(dnstr)
- transport.load_transport(samdb)
+ transport.load_transport(self.samdb)
# Assign this transport to table
# and index by dn
@@ -99,27 +111,96 @@ class KCC:
return
+ def load_all_sitelinks(self):
+ """Loads the inter-site siteLink objects
+
+ ::returns: Raises an Exception on error
+ """
+ try:
+ res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" % \
+ self.samdb.get_config_basedn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression="(objectClass=siteLink)")
+ except ldb.LdbError, (enum, estr):
+ raise Exception("Unable to find inter-site siteLinks - (%s)" % estr)
+
+ for msg in res:
+ dnstr = str(msg.dn)
+
+ # already loaded
+ if dnstr in self.sitelink_table.keys():
+ continue
+
+ sitelink = SiteLink(dnstr)
+
+ sitelink.load_sitelink(self.samdb)
+
+ # Assign this siteLink to table
+ # and index by dn
+ self.sitelink_table[dnstr] = sitelink
+
+ return
+
+ def get_sitelink(self, site1_dnstr, site2_dnstr):
+ """Return the siteLink (if it exists) that connects the
+ two input site DNs
+ """
+ for sitelink in self.sitelink_table.values():
+ if sitelink.is_sitelink(site1_dnstr, site2_dnstr):
+ return sitelink
+ return None
+
def load_my_site(self):
"""Loads the Site class for the local DSA
- Raises an Exception on error
+
+ ::returns: Raises an Exception on error
"""
- self.my_site_dnstr = "CN=%s,CN=Sites,%s" % (samdb.server_site_name(),
- samdb.get_config_basedn())
+ self.my_site_dnstr = "CN=%s,CN=Sites,%s" % \
+ (self.samdb.server_site_name(),
+ self.samdb.get_config_basedn())
site = Site(self.my_site_dnstr)
- site.load_site(samdb)
+ site.load_site(self.samdb)
self.site_table[self.my_site_dnstr] = site
self.my_site = site
return
+ def load_all_sites(self):
+ """Discover all sites and instantiate and load each
+ NTDS Site settings.
+
+ ::returns: Raises an Exception on error
+ """
+ try:
+ res = self.samdb.search("CN=Sites,%s" %
+ self.samdb.get_config_basedn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression="(objectClass=site)")
+ except ldb.LdbError, (enum, estr):
+ raise Exception("Unable to find sites - (%s)" % estr)
+
+ for msg in res:
+ sitestr = str(msg.dn)
+
+ # already loaded
+ if sitestr in self.site_table.keys():
+ continue
+
+ site = Site(sitestr)
+ site.load_site(self.samdb)
+
+ self.site_table[sitestr] = site
+ return
+
def load_my_dsa(self):
"""Discover my nTDSDSA dn thru the rootDSE entry
- Raises an Exception on error.
+
+ ::returns: Raises an Exception on error.
"""
dn = ldb.Dn(self.samdb, "")
try:
- res = samdb.search(base=dn, scope=ldb.SCOPE_BASE,
- attrs=["dsServiceName"])
+ res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
+ attrs=["dsServiceName"])
except ldb.LdbError, (enum, estr):
raise Exception("Unable to find my nTDSDSA - (%s)" % estr)
@@ -130,10 +211,12 @@ class KCC:
def load_all_partitions(self):
"""Discover all NCs thru the Partitions dn and
- instantiate and load the NCs. Each NC is inserted
- into the part_table by partition dn string (not
- the nCName dn string)
- Raises an Exception on error
+ instantiate and load the NCs.
+
+ Each NC is inserted into the part_table by partition
+ dn string (not the nCName dn string)
+
+ ::returns: Raises an Exception on error
"""
try:
res = self.samdb.search("CN=Partitions,%s" %
@@ -157,7 +240,7 @@ class KCC:
def should_be_present_test(self):
"""Enumerate all loaded partitions and DSAs in local
- site and test if NC should be present as replica
+ site and test if NC should be present as replica
"""
for partdn, part in self.part_table.items():
for dsadn, dsa in self.my_site.dsa_table.items():
@@ -172,9 +255,9 @@ class KCC:
def is_stale_link_connection(self, target_dsa):
"""Returns False if no tuple z exists in the kCCFailedLinks or
- kCCFailedConnections variables such that z.UUIDDsa is the
- objectGUID of the target dsa, z.FailureCount > 0, and
- the current time - z.TimeFirstFailure > 2 hours.
+ kCCFailedConnections variables such that z.UUIDDsa is the
+ objectGUID of the target dsa, z.FailureCount > 0, and
+ the current time - z.TimeFirstFailure > 2 hours.
"""
# XXX - not implemented yet
return False
@@ -183,13 +266,147 @@ class KCC:
# XXX - not implemented yet
return
- def remove_unneeded_ntdsconn(self):
- # XXX - not implemented yet
+ def remove_unneeded_ntdsconn(self, all_connected):
+ """Removes unneeded NTDS Connections after computation
+ of KCC intra and inter-site topology has finished.
+ """
+ mydsa = self.my_dsa
+
+ # Loop thru connections
+ for cn_dnstr, cn_conn in mydsa.connect_table.items():
+
+ s_dnstr = cn_conn.get_from_dnstr()
+ if s_dnstr is None:
+ cn_conn.to_be_deleted = True
+ continue
+
+ # Get the source DSA no matter what site
+ s_dsa = self.get_dsa(s_dnstr)
+
+ # Check if the DSA is in our site
+ if self.my_site.same_site(s_dsa):
+ same_site = True
+ else:
+ same_site = False
+
+ # Given an nTDSConnection object cn, if the DC with the
+ # nTDSDSA object dc that is the parent object of cn and
+ # the DC with the nTDSDA object referenced by cn!fromServer
+ # are in the same site, the KCC on dc deletes cn if all of
+ # the following are true:
+ #
+ # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
+ #
+ # No site settings object s exists for the local DC's site, or
+ # bit NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED is clear in
+ # s!options.
+ #
+ # Another nTDSConnection object cn2 exists such that cn and
+ # cn2 have the same parent object, cn!fromServer = cn2!fromServer,
+ # and either
+ #
+ # cn!whenCreated < cn2!whenCreated
+ #
+ # cn!whenCreated = cn2!whenCreated and
+ # cn!objectGUID < cn2!objectGUID
+ #
+ # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
+ if same_site:
+ if cn_conn.is_generated() == False:
+ continue
+
+ if self.my_site.is_cleanup_ntdsconn_disabled() == True:
+ continue
+
+ # Loop thru connections looking for a duplicate that
+ # fulfills the previous criteria
+ lesser = False
+
+ for cn2_dnstr, cn2_conn in mydsa.connect_table.items():
+ if cn2_conn is cn_conn:
+ continue
+
+ s2_dnstr = cn2_conn.get_from_dnstr()
+ if s2_dnstr is None:
+ continue
+
+ # If the NTDS Connections has a different
+ # fromServer field then no match
+ if s2_dnstr != s_dnstr:
+ continue
+
+ lesser = (cn_conn.whenCreated < cn2_conn.whenCreated or
+ (cn_conn.whenCreated == cn2_conn.whenCreated and
+ cmp(cn_conn.guid, cn2_conn.guid) < 0))
+
+ if lesser == True:
+ break
+
+ if lesser and cn_conn.is_rodc_topology() == False:
+ cn_conn.to_be_deleted = True
+
+ # Given an nTDSConnection object cn, if the DC with the nTDSDSA
+ # object dc that is the parent object of cn and the DC with
+ # the nTDSDSA object referenced by cn!fromServer are in
+ # different sites, a KCC acting as an ISTG in dc's site
+ # deletes cn if all of the following are true:
+ #
+ # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
+ #
+ # cn!fromServer references an nTDSDSA object for a DC
+ # in a site other than the local DC's site.
+ #
+ # The keepConnections sequence returned by
+ # CreateIntersiteConnections() does not contain
+ # cn!objectGUID, or cn is "superseded by" (see below)
+ # another nTDSConnection cn2 and keepConnections
+ # contains cn2!objectGUID.
+ #
+ # The return value of CreateIntersiteConnections()
+ # was true.
+ #
+ # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in
+ # cn!options
+ #
+ else: # different site
+
+ if mydsa.is_istg() == False:
+ continue
+
+ if cn_conn.is_generated() == False:
+ continue
+
+ if self.keep_connection(cn_conn) == True:
+ continue
+
+ # XXX - To be implemented
+
+ if all_connected == False:
+ continue
+
+ if cn_conn.is_rodc_topology() == False:
+ cn_conn.to_be_deleted = True
+
+
+ if opts.readonly:
+ for dnstr, connect in mydsa.connect_table.items():
+ if connect.to_be_deleted == True:
+ logger.info("TO BE DELETED:\n%s" % connect)
+ if connect.to_be_added == True:
+ logger.info("TO BE ADDED:\n%s" % connect)
+
+ # Peform deletion from our tables but perform
+ # no database modification
+ mydsa.commit_connections(self.samdb, ro=True)
+ else:
+ # Commit any modified connections
+ mydsa.commit_connections(self.samdb)
+
return
def get_dsa_by_guidstr(self, guidstr):
"""Given a DSA guid string, consule all sites looking
- for the corresponding DSA and return it.
+ for the corresponding DSA and return it.
"""
for site in self.site_table.values():
dsa = site.get_dsa_by_guidstr(guidstr)
@@ -199,7 +416,7 @@ class KCC:
def get_dsa(self, dnstr):
"""Given a DSA dn string, consule all sites looking
- for the corresponding DSA and return it.
+ for the corresponding DSA and return it.
"""
for site in self.site_table.values():
dsa = site.get_dsa(dnstr)
@@ -209,16 +426,18 @@ class KCC:
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
+ 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:
@@ -229,7 +448,7 @@ class KCC:
s_dnstr = s_dsa.dsa_dnstr
update = 0x0
- if self.my_site.get_dsa(s_dnstr) is s_dsa:
+ if self.my_site.same_site(s_dsa):
same_site = True
else:
same_site = False
@@ -424,7 +643,7 @@ class KCC:
t_repsFrom.transport_guid = x_transport.guid
# See (NOTE MS-TECH INCORRECT) above
- if x_transport.addr_attr == "dNSHostName":
+ if x_transport.address_attr == "dNSHostName":
if t_repsFrom.version == 0x1:
if t_repsFrom.dns_name1 is None or \
@@ -440,21 +659,21 @@ class KCC:
else:
# MS tech specification says we retrieve the named
- # attribute in "addr_attr" from the parent of the
- # DSA object
+ # attribute in "transportAddressAttribute" from the parent of
+ # the DSA object
try:
pdnstr = s_dsa.get_parent_dnstr()
- attrs = [ x_transport.addr_attr ]
+ attrs = [ x_transport.address_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))
+ (x_transport.address_attr, pdnstr, estr))
msg = res[0]
- nastr = str(msg[x_transport.addr_attr][0])
+ nastr = str(msg[x_transport.address_attr][0])
# See (NOTE MS-TECH INCORRECT) above
if t_repsFrom.version == 0x1:
@@ -474,14 +693,79 @@ class KCC:
logger.debug("modify_repsFrom(): %s" % t_repsFrom)
return
+ def is_repsFrom_implied(self, n_rep, cn_conn):
+ """Given a NC replica and NTDS Connection, determine if the connection
+ implies a repsFrom tuple should be present from the source DSA listed
+ in the connection to the naming context
+
+ :param n_rep: NC replica
+ :param conn: NTDS Connection
+ ::returns (True || False), source DSA:
+ """
+ # 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)
+
+ # No DSA matching this source DN string?
+ if s_dsa == None:
+ return False, None
+
+ # 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:
+ return False, None
+
+ # 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.
+ #
+ implied = (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 implied:
+ return True, s_dsa
+ else:
+ return False, None
+
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.
+ replicas on the local DC to match those implied by
+ nTDSConnection objects.
"""
- logger.debug("translate_ntdsconn(): enter mydsa:\n%s" % self.my_dsa)
+ logger.debug("translate_ntdsconn(): enter")
- if self.my_dsa.should_translate_ntdsconn() == False:
+ if self.my_dsa.is_translate_ntdsconn_disabled():
return
current_rep_table, needed_rep_table = self.my_dsa.get_rep_tables()
@@ -489,12 +773,6 @@ class KCC:
# 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.
#
@@ -508,26 +786,16 @@ class KCC:
# 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
+ # If we have the replica and its not needed
+ # then we add it to the "to be deleted" 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():
+ for dnstr, n_rep in needed_rep_table.items():
# load any repsFrom and fsmo roles as we'll
# need them during connection translation
@@ -591,22 +859,8 @@ class KCC:
# 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:
+ implied, s_dsa = self.is_repsFrom_implied(n_rep, cn_conn)
+ if implied == False:
continue
# Loop thru the existing repsFrom tupples (if any) and
@@ -623,44 +877,6 @@ class KCC:
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)
@@ -673,22 +889,648 @@ class KCC:
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:
+ if opts.readonly:
+ # Display any to be deleted or modified repsFrom
+ text = n_rep.dumpstr_to_be_deleted()
+ if text:
+ logger.info("TO BE DELETED:\n%s" % text)
+ text = n_rep.dumpstr_to_be_modified()
+ if text:
+ logger.info("TO BE MODIFIED:\n%s" % text)
+
+ # Peform deletion from our tables but perform
+ # no database modification
+ n_rep.commit_repsFrom(self.samdb, ro=True)
+ else:
+ # Commit any modified repsFrom to the NC replica
n_rep.commit_repsFrom(self.samdb)
return
+ def keep_connection(self, cn_conn):
+ """Determines if the connection is meant to be kept during the
+ pruning of unneeded connections operation.
+
+ Consults the keep_connection_list[] which was built during
+ intersite NC replica graph computation.
+
+ ::returns (True or False): if (True) connection should not be pruned
+ """
+ if cn_conn in self.keep_connection_list:
+ return True
+ return False
+
+ def merge_failed_links(self):
+ """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
+ The KCC on a writable DC attempts to merge the link and connection
+ failure information from bridgehead DCs in its own site to help it
+ identify failed bridgehead DCs.
+ """
+ # MS-TECH Ref 6.2.2.3.2 Merge of kCCFailedLinks and kCCFailedLinks
+ # from Bridgeheads
+
+ # XXX - not implemented yet
+ return
+
+ def setup_graph(self):
+ """Set up a GRAPH, populated with a VERTEX for each site
+ object, a MULTIEDGE for each siteLink object, and a
+ MUTLIEDGESET for each siteLinkBridge object (or implied
+ siteLinkBridge).
+
+ ::returns: a new graph
+ """
+ # XXX - not implemented yet
+ return None
+
+ def get_bridgehead(self, site, part, transport, \
+ partial_ok, detect_failed):
+ """Get a bridghead DC.
+
+ :param site: site object representing for which a bridgehead
+ DC is desired.
+ :param part: crossRef for NC to replicate.
+ :param transport: interSiteTransport object for replication
+ traffic.
+ :param partial_ok: True if a DC containing a partial
+ replica or a full replica will suffice, False if only
+ a full replica will suffice.
+ :param detect_failed: True to detect failed DCs and route
+ replication traffic around them, False to assume no DC
+ has failed.
+ ::returns: dsa object for the bridgehead DC or None
+ """
+
+ bhs = self.get_all_bridgeheads(site, part, transport, \
+ partial_ok, detect_failed)
+ if len(bhs) == 0:
+ logger.debug("get_bridgehead: exit\n\tsitedn=%s\n\tbhdn=None" % \
+ site.site_dnstr)
+ return None
+ else:
+ logger.debug("get_bridgehead: exit\n\tsitedn=%s\n\tbhdn=%s" % \
+ (site.site_dnstr, bhs[0].dsa_dnstr))
+ return bhs[0]
+
+ def get_all_bridgeheads(self, site, part, transport, \
+ partial_ok, detect_failed):
+ """Get all bridghead DCs satisfying the given criteria
+
+ :param site: site object representing the site for which
+ bridgehead DCs are desired.
+ :param part: partition for NC to replicate.
+ :param transport: interSiteTransport object for
+ replication traffic.
+ :param partial_ok: True if a DC containing a partial
+ replica or a full replica will suffice, False if
+ only a full replica will suffice.
+ :param detect_ok: True to detect failed DCs and route
+ replication traffic around them, FALSE to assume
+ no DC has failed.
+ ::returns: list of dsa object for available bridgehead
+ DCs or None
+ """
+
+ bhs = []
+
+ logger.debug("get_all_bridgeheads: %s" % transport)
+
+ for key, dsa in site.dsa_table.items():
+
+ pdnstr = dsa.get_parent_dnstr()
+
+ # IF t!bridgeheadServerListBL has one or more values and
+ # t!bridgeheadServerListBL does not contain a reference
+ # to the parent object of dc then skip dc
+ if len(transport.bridgehead_list) != 0 and \
+ pdnstr not in transport.bridgehead_list:
+ continue
+
+ # IF dc is in the same site as the local DC
+ # IF a replica of cr!nCName is not in the set of NC replicas
+ # that "should be present" on dc or a partial replica of the
+ # NC "should be present" but partialReplicasOkay = FALSE
+ # Skip dc
+ if self.my_site.same_site(dsa):
+ needed, ro, partial = part.should_be_present(dsa)
+ if needed == False or (partial == True and partial_ok == False):
+ continue
+
+ # ELSE
+ # IF an NC replica of cr!nCName is not in the set of NC
+ # replicas that "are present" on dc or a partial replica of
+ # the NC "is present" but partialReplicasOkay = FALSE
+ # Skip dc
+ else:
+ rep = dsa.get_current_replica(part.nc_dnstr)
+ if rep is None or (rep.is_partial() and partial_ok == False):
+ continue
+
+ # IF AmIRODC() and cr!nCName corresponds to default NC then
+ # Let dsaobj be the nTDSDSA object of the dc
+ # IF dsaobj.msDS-Behavior-Version < DS_BEHAVIOR_WIN2008
+ # Skip dc
+ if self.my_dsa.is_ro() and part.is_default():
+ if dsa.is_minimum_behavior(DS_BEHAVIOR_WIN2008) == False:
+ continue
+
+ # IF t!name != "IP" and the parent object of dc has no value for
+ # the attribute specified by t!transportAddressAttribute
+ # Skip dc
+ if transport.name != "IP":
+ # MS tech specification says we retrieve the named
+ # attribute in "transportAddressAttribute" from the parent
+ # of the DSA object
+ try:
+ attrs = [ transport.address_attr ]
+
+ res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
+ attrs=attrs)
+ except ldb.ldbError, (enum, estr):
+ continue
+
+ msg = res[0]
+ nastr = str(msg[transport.address_attr][0])
+
+ # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
+ # Skip dc
+ if self.is_bridgehead_failed(dsa, detect_failed) == True:
+ continue
+
+ logger.debug("get_all_bridgeheads: dsadn=%s" % dsa.dsa_dnstr)
+ bhs.append(dsa)
+
+ # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
+ # s!options
+ # SORT bhs such that all GC servers precede DCs that are not GC
+ # servers, and otherwise by ascending objectGUID
+ # ELSE
+ # SORT bhs in a random order
+ if site.is_random_bridgehead_disabled() == True:
+ bhs.sort(sort_dsa_by_gc_and_guid)
+ else:
+ random.shuffle(bhs)
+
+ return bhs
+
+
+ def is_bridgehead_failed(self, dsa, detect_failed):
+ """Determine whether a given DC is known to be in a failed state
+ ::returns: True if and only if the DC should be considered failed
+ """
+ # XXX - not implemented yet
+ return False
+
+ def create_connection(self, part, rbh, rsite, transport, \
+ lbh, lsite, link_opt, link_sched, \
+ partial_ok, detect_failed):
+ """Create an nTDSConnection object with the given parameters
+ if one does not already exist.
+
+ :param part: crossRef object for the NC to replicate.
+ :param rbh: nTDSDSA object for DC to act as the
+ IDL_DRSGetNCChanges server (which is in a site other
+ than the local DC's site).
+ :param rsite: site of the rbh
+ :param transport: interSiteTransport object for the transport
+ to use for replication traffic.
+ :param lbh: nTDSDSA object for DC to act as the
+ IDL_DRSGetNCChanges client (which is in the local DC's site).
+ :param lsite: site of the lbh
+ :param link_opt: Replication parameters (aggregated siteLink options, etc.)
+ :param link_sched: Schedule specifying the times at which
+ to begin replicating.
+ :partial_ok: True if bridgehead DCs containing partial
+ replicas of the NC are acceptable.
+ :param detect_failed: True to detect failed DCs and route
+ replication traffic around them, FALSE to assume no DC
+ has failed.
+ """
+ rbhs_all = self.get_all_bridgeheads(rsite, part, transport, \
+ partial_ok, False)
+
+ # MS-TECH says to compute rbhs_avail but then doesn't use it
+ # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport, \
+ # partial_ok, detect_failed)
+
+ lbhs_all = self.get_all_bridgeheads(lsite, part, transport, \
+ partial_ok, False)
+
+ # MS-TECH says to compute lbhs_avail but then doesn't use it
+ # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport, \
+ # partial_ok, detect_failed)
+
+ # FOR each nTDSConnection object cn such that the parent of cn is
+ # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
+ for ldsa in lbhs_all:
+ for cn in ldsa.connect_table.values():
+
+ rdsa = None
+ for rdsa in rbhs_all:
+ if cn.from_dnstr == rdsa.dsa_dnstr:
+ break
+
+ if rdsa is None:
+ continue
+
+ # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
+ # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
+ # cn!transportType references t
+ if cn.is_generated() == True and \
+ cn.is_rodc_topology() == False and \
+ cn.transport_dnstr == transport.dnstr:
+
+ # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
+ # cn!options and cn!schedule != sch
+ # Perform an originating update to set cn!schedule to
+ # sched
+ if cn.is_user_owned_schedule() == False and \
+ cn.is_equivalent_schedule(link_sched) == False:
+ cn.schedule = link_sched
+ cn.set_modified(True)
+
+ # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
+ # NTDSCONN_OPT_USE_NOTIFY are set in cn
+ if cn.is_override_notify_default() == True and \
+ cn.is_use_notify() == True:
+
+ # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
+ # ri.Options
+ # Perform an originating update to clear bits
+ # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
+ # NTDSCONN_OPT_USE_NOTIFY in cn!options
+ if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
+ cn.options &= \
+ ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT | \
+ dsdb.NTDSCONN_OPT_USE_NOTIFY)
+ cn.set_modified(True)
+
+ # ELSE
+ else:
+
+ # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
+ # ri.Options
+ # Perform an originating update to set bits
+ # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
+ # NTDSCONN_OPT_USE_NOTIFY in cn!options
+ if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
+ cn.options |= \
+ (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT | \
+ dsdb.NTDSCONN_OPT_USE_NOTIFY)
+ cn.set_modified(True)
+
+
+ # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
+ if cn.is_twoway_sync() == True:
+
+ # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
+ # ri.Options
+ # Perform an originating update to clear bit
+ # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
+ if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
+ cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
+ cn.set_modified(True)
+
+ # ELSE
+ else:
+
+ # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
+ # ri.Options
+ # Perform an originating update to set bit
+ # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
+ if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
+ cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
+ cn.set_modified(True)
+
+
+ # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
+ # in cn!options
+ if cn.is_intersite_compression_disabled() == True:
+
+ # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
+ # in ri.Options
+ # Perform an originating update to clear bit
+ # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
+ # cn!options
+ if (link_opt & \
+ dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0:
+ cn.options &= \
+ ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
+ cn.set_modified(True)
+
+ # ELSE
+ else:
+ # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
+ # ri.Options
+ # Perform an originating update to set bit
+ # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
+ # cn!options
+ if (link_opt & \
+ dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0:
+ cn.options |= \
+ dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
+ cn.set_modified(True)
+
+ # Display any modified connection
+ if opts.readonly:
+ if cn.to_be_modified == True:
+ logger.info("TO BE MODIFIED:\n%s" % cn)
+
+ ldsa.commit_connections(self.samdb, ro=True)
+ else:
+ ldsa.commit_connections(self.samdb)
+ # ENDFOR
+
+ valid_connections = 0
+
+ # FOR each nTDSConnection object cn such that cn!parent is
+ # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
+ for ldsa in lbhs_all:
+ for cn in ldsa.connect_table.values():
+
+ rdsa = None
+ for rdsa in rbhs_all:
+ if cn.from_dnstr == rdsa.dsa_dnstr:
+ break
+
+ if rdsa is None:
+ continue
+
+ # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
+ # cn!transportType references t) and
+ # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
+ if (cn.is_generated() == False or \
+ cn.transport_dnstr == transport.dnstr) and \
+ cn.is_rodc_topology() == False:
+
+ # LET rguid be the objectGUID of the nTDSDSA object
+ # referenced by cn!fromServer
+ # LET lguid be (cn!parent)!objectGUID
+
+ # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
+ # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
+ # Increment cValidConnections by 1
+ if self.is_bridgehead_failed(rdsa, detect_failed) == False and \
+ self.is_bridgehead_failed(ldsa, detect_failed) == False:
+ valid_connections += 1
+
+ # IF keepConnections does not contain cn!objectGUID
+ # APPEND cn!objectGUID to keepConnections
+ if self.keep_connection(cn) == False:
+ self.keep_connection_list.append(cn)
+
+ # ENDFOR
+
+ # IF cValidConnections = 0
+ if valid_connections == 0:
+
+ # LET opt be NTDSCONN_OPT_IS_GENERATED
+ opt = dsdb.NTDSCONN_OPT_IS_GENERATED
+
+ # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
+ # SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
+ # NTDSCONN_OPT_USE_NOTIFY in opt
+ if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
+ opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT | \
+ dsdb.NTDSCONN_USE_NOTIFY)
+
+ # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
+ # SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
+ if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
+ opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
+
+ # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
+ # ri.Options
+ # SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
+ if (link_opt & \
+ dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0:
+ opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
+
+ # Perform an originating update to create a new nTDSConnection
+ # object cn that is a child of lbh, cn!enabledConnection = TRUE,
+ # cn!options = opt, cn!transportType is a reference to t,
+ # cn!fromServer is a reference to rbh, and cn!schedule = sch
+ cn = lbh.new_connection(opt, 0, transport, lbh.dsa_dnstr, link_sched)
+
+ # Display any added connection
+ if opts.readonly:
+ if cn.to_be_added == True:
+ logger.info("TO BE ADDED:\n%s" % cn)
+
+ lbh.commit_connections(self.samdb, ro=True)
+ else:
+ lbh.commit_connections(self.samdb)
+
+ # APPEND cn!objectGUID to keepConnections
+ if self.keep_connection(cn) == False:
+ self.keep_connection_list.append(cn)
+
+ return
+
+
+ def create_connections(self, graph, part, detect_failed):
+ """Construct an NC replica graph for the NC identified by
+ the given crossRef, then create any additional nTDSConnection
+ objects required.
+
+ :param graph: site graph.
+ :param part: crossRef object for NC.
+ :param detect_failed: True to detect failed DCs and route
+ replication traffic around them, False to assume no DC
+ has failed.
+
+ Modifies self.keep_connection_list by adding any connections
+ deemed to be "in use".
+
+ ::returns: (all_connected, found_failed_dc)
+ (all_connected) True if the resulting NC replica graph
+ connects all sites that need to be connected.
+ (found_failed_dc) True if one or more failed DCs were
+ detected.
+ """
+ all_connected = True
+ found_failed = False
+
+ logger.debug("create_connections(): enter\n\tpartdn=%s\n\tdetect_failed=%s" % \
+ (part.nc_dnstr, detect_failed))
+
+ # XXX - This is a highly abbreviated function from the MS-TECH
+ # ref. It creates connections between bridgeheads to all
+ # sites that have appropriate replicas. Thus we are not
+ # creating a minimum cost spanning tree but instead
+ # producing a fully connected tree. This should produce
+ # a full (albeit not optimal cost) replication topology.
+ my_vertex = Vertex(self.my_site, part)
+ my_vertex.color_vertex()
+
+ # No NC replicas for this NC in the site of the local DC,
+ # so no nTDSConnection objects need be created
+ if my_vertex.is_white():
+ return all_connected, found_failed
+
+ # LET partialReplicaOkay be TRUE if and only if
+ # localSiteVertex.Color = COLOR.BLACK
+ if my_vertex.is_black():
+ partial_ok = True
+ else:
+ partial_ok = False
+
+ # Utilize the IP transport only for now
+ transport = None
+ for transport in self.transport_table.values():
+ if transport.name == "IP":
+ break
+
+ if transport is None:
+ raise Exception("Unable to find inter-site transport for IP")
+
+ for rsite in self.site_table.values():
+
+ # We don't make connections to our own site as that
+ # is intrasite topology generator's job
+ if rsite is self.my_site:
+ continue
+
+ # Determine bridgehead server in remote site
+ rbh = self.get_bridgehead(rsite, part, transport,
+ partial_ok, detect_failed)
+
+ # RODC acts as an BH for itself
+ # IF AmIRODC() then
+ # LET lbh be the nTDSDSA object of the local DC
+ # ELSE
+ # LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
+ # cr, t, partialReplicaOkay, detectFailedDCs)
+ if self.my_dsa.is_ro():
+ lsite = self.my_site
+ lbh = self.my_dsa
+ else:
+ lsite = self.my_site
+ lbh = self.get_bridgehead(lsite, part, transport,
+ partial_ok, detect_failed)
+
+ # Find the siteLink object that enumerates the connection
+ # between the two sites if it is present
+ sitelink = self.get_sitelink(lsite.site_dnstr, rsite.site_dnstr)
+ if sitelink is None:
+ link_opt = 0x0
+ link_sched = None
+ else:
+ link_opt = sitelink.options
+ link_sched = sitelink.schedule
+
+ self.create_connection(part, rbh, rsite, transport,
+ lbh, lsite, link_opt, link_sched,
+ partial_ok, detect_failed)
+
+ return all_connected, found_failed
+
+ def create_intersite_connections(self):
+ """Computes an NC replica graph for each NC replica that "should be
+ present" on the local DC or "is present" on any DC in the same site
+ as the local DC. For each edge directed to an NC replica on such a
+ DC from an NC replica on a DC in another site, the KCC creates an
+ nTDSConnection object to imply that edge if one does not already
+ exist.
+
+ Modifies self.keep_connection_list - A list of nTDSConnection
+ objects for edges that are directed
+ to the local DC's site in one or more NC replica graphs.
+
+ returns: True if spanning trees were created for all NC replica
+ graphs, otherwise False.
+ """
+ all_connected = True
+ self.keep_connection_list = []
+
+ # LET crossRefList be the set containing each object o of class
+ # crossRef such that o is a child of the CN=Partitions child of the
+ # config NC
+
+ # FOR each crossRef object cr in crossRefList
+ # IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
+ # is clear in cr!systemFlags, skip cr.
+ # LET g be the GRAPH return of SetupGraph()
+
+ for part in self.part_table.values():
+
+ if part.is_enabled() == False:
+ continue
+
+ if part.is_foreign() == True:
+ continue
+
+ graph = self.setup_graph()
+
+ # Create nTDSConnection objects, routing replication traffic
+ # around "failed" DCs.
+ found_failed = False
+
+ connected, found_failed = self.create_connections(graph, part, True)
+
+ if connected == False:
+ all_connected = False
+
+ if found_failed:
+ # One or more failed DCs preclude use of the ideal NC
+ # replica graph. Add connections for the ideal graph.
+ self.create_connections(graph, part, False)
+
+ return all_connected
+
def intersite(self):
"""The head method for generating the inter-site KCC replica
- connection graph and attendant nTDSConnection objects
- in the samdb
+ connection graph and attendant nTDSConnection objects
+ in the samdb.
+
+ Produces self.keep_connection_list[] of NTDS Connections
+ that should be kept during subsequent pruning process.
+
+ ::return (True or False): (True) if the produced NC replica
+ graph connects all sites that need to be connected
"""
- # XXX - not implemented yet
+
+ # Retrieve my DSA
+ mydsa = self.my_dsa
+ mysite = self.my_site
+ all_connected = True
+
+ logger.debug("intersite(): enter")
+
+ # Determine who is the ISTG
+ if opts.readonly:
+ mysite.select_istg(self.samdb, mydsa, ro=True)
+ else:
+ mysite.select_istg(self.samdb, mydsa, ro=False)
+
+ # Test whether local site has topology disabled
+ if mysite.is_intersite_topology_disabled():
+ logger.debug("intersite(): exit disabled all_connected=%d" % \
+ all_connected)
+ return all_connected
+
+ if mydsa.is_istg() == False:
+ logger.debug("intersite(): exit not istg all_connected=%d" % \
+ all_connected)
+ return all_connected
+
+ self.merge_failed_links()
+
+ # For each NC with an NC replica that "should be present" on the
+ # local DC or "is present" on any DC in the same site as the
+ # local DC, the KCC constructs a site graph--a precursor to an NC
+ # replica graph. The site connectivity for a site graph is defined
+ # by objects of class interSiteTransport, siteLink, and
+ # siteLinkBridge in the config NC.
+
+ all_connected = self.create_intersite_connections()
+
+ logger.debug("intersite(): exit all_connected=%d" % all_connected)
+ return all_connected
def update_rodc_connection(self):
"""Runs when the local DC is an RODC and updates the RODC NTFRS
- connection object.
+ connection object.
"""
# Given an nTDSConnection object cn1, such that cn1.options contains
# NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
@@ -703,14 +1545,19 @@ class KCC:
# XXX - not implemented yet
+ return
+
def intrasite_max_node_edges(self, node_count):
"""Returns the maximum number of edges directed to a node in
- the intrasite replica graph. The KCC does not create more
- than 50 edges directed to a single DC. To optimize replication,
- we compute that each node should have n+2 total edges directed
- to it such that (n) is the smallest non-negative integer
- satisfying (node_count <= 2*(n*n) + 6*n + 7)
- :param node_count: total number of nodes in the replica graph
+ the intrasite replica graph.
+
+ The KCC does not create more
+ than 50 edges directed to a single DC. To optimize replication,
+ we compute that each node should have n+2 total edges directed
+ to it such that (n) is the smallest non-negative integer
+ satisfying (node_count <= 2*(n*n) + 6*n + 7)
+
+ :param node_count: total number of nodes in the replica graph
"""
n = 0
while True:
@@ -759,7 +1606,7 @@ class KCC:
# Create a NCReplica that matches what the local replica
# should say. We'll use this below in our r_list
l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid, \
- nc_x.nc_dnstr, nc_x.nc_guid, nc_x.nc_sid)
+ nc_x.nc_dnstr)
l_of_x.identify_by_basedn(self.samdb)
@@ -1018,20 +1865,20 @@ class KCC:
def intrasite(self):
"""The head method for generating the intra-site KCC replica
- connection graph and attendant nTDSConnection objects
- in the samdb
+ connection graph and attendant nTDSConnection objects
+ in the samdb
"""
# Retrieve my DSA
mydsa = self.my_dsa
- logger.debug("intrasite(): enter mydsa:\n%s" % mydsa)
+ logger.debug("intrasite(): enter")
# Test whether local site has topology disabled
mysite = self.site_table[self.my_site_dnstr]
if mysite.is_intrasite_topology_disabled():
return
- detect_stale = mysite.should_detect_stale()
+ detect_stale = (mysite.is_detect_stale_disabled() == False)
# Loop thru all the partitions.
for partdn, part in self.part_table.items():
@@ -1072,22 +1919,50 @@ class KCC:
True, \
False) # don't detect stale
- # Commit any newly created connections to the samdb
- if opts.readonly is None:
- mydsa.commit_connection_table(self.samdb)
+ if opts.readonly:
+ # Display any to be added or modified repsFrom
+ for dnstr, connect in mydsa.connect_table.items():
+ if connect.to_be_deleted == True:
+ logger.info("TO BE DELETED:\n%s" % connect)
+ if connect.to_be_modified == True:
+ logger.info("TO BE MODIFIED:\n%s" % connect)
+ if connect.to_be_added == True:
+ logger.info("TO BE ADDED:\n%s" % connect)
+
+ mydsa.commit_connections(self.samdb, ro=True)
+ else:
+ # Commit any newly created connections to the samdb
+ mydsa.commit_connections(self.samdb)
+
+ return
- def run(self):
+ def run(self, dburl, lp, creds):
"""Method to perform a complete run of the KCC and
- produce an updated topology for subsequent NC replica
- syncronization between domain controllers
+ produce an updated topology for subsequent NC replica
+ syncronization between domain controllers
"""
+ # We may already have a samdb setup if we are
+ # currently importing an ldif for a test run
+ if self.samdb is None:
+ try:
+ self.samdb = SamDB(url=lp.samdb_url(),
+ session_info=system_session(),
+ credentials=creds, lp=lp)
+
+ except ldb.LdbError, (num, msg):
+ logger.error("Unable to open sam database %s : %s" % \
+ (lp.samdb_url(), msg))
+ return 1
+
try:
# Setup
self.load_my_site()
self.load_my_dsa()
+ self.load_all_sites()
self.load_all_partitions()
self.load_all_transports()
+ self.load_all_sitelinks()
# These are the published steps (in order) for the
# MS-TECH description of the KCC algorithm
@@ -1099,10 +1974,10 @@ class KCC:
self.intrasite()
# Step 3
- self.intersite()
+ all_connected = self.intersite()
# Step 4
- self.remove_unneeded_ntdsconn()
+ self.remove_unneeded_ntdsconn(all_connected)
# Step 5
self.translate_ntdsconn()
@@ -1119,19 +1994,396 @@ class KCC:
return 0
+ def import_ldif(self, dburl, lp, creds, ldif_file):
+ """Routine to import all objects and attributes that are relevent
+ to the KCC algorithms from a previously exported LDIF file.
+
+ The point of this function is to allow a programmer/debugger to
+ import an LDIF file with non-security relevent information that
+ was previously extracted from a DC database. The LDIF file is used
+ to create a temporary abbreviated database. The KCC algorithm can
+ then run against this abbreviated database for debug or test
+ verification that the topology generated is computationally the
+ same between different OSes and algorithms.
+
+ :param dburl: path to the temporary abbreviated db to create
+ :param ldif_file: path to the ldif file to import
+ """
+ if os.path.exists(dburl):
+ logger.error("Specify a database (%s) that doesn't already exist." %
+ dburl)
+ return 1
+
+ # Use ["modules:"] as we are attempting to build a sam
+ # database as opposed to start it here.
+ self.samdb = Ldb(url=dburl, session_info=system_session(),
+ lp=lp, options=["modules:"])
+
+ self.samdb.transaction_start()
+ try:
+ data = read_and_sub_file(ldif_file, None)
+ self.samdb.add_ldif(data, None)
+
+ except Exception, estr:
+ logger.error("%s" % estr)
+ self.samdb.transaction_cancel()
+ return 1
+ else:
+ self.samdb.transaction_commit()
+
+ self.samdb = None
+
+ # We have an abbreviated list of options here because we have built
+ # an abbreviated database. We use the rootdse and extended-dn
+ # modules only during this re-open
+ self.samdb = SamDB(url=dburl, session_info=system_session(),
+ credentials=creds, lp=lp,
+ options=["modules:rootdse,extended_dn_out_ldb"])
+ return 0
+
+ def export_ldif(self, dburl, lp, creds, ldif_file):
+ """Routine to extract all objects and attributes that are relevent
+ to the KCC algorithms from a DC database.
+
+ The point of this function is to allow a programmer/debugger to
+ extract an LDIF file with non-security relevent information from
+ a DC database. The LDIF file can then be used to "import" via
+ the import_ldif() function this file into a temporary abbreviated
+ database. The KCC algorithm can then run against this abbreviated
+ database for debug or test verification that the topology generated
+ is computationally the same between different OSes and algorithms.
+
+ :param dburl: LDAP database URL to extract info from
+ :param ldif_file: output LDIF file name to create
+ """
+ try:
+ self.samdb = SamDB(url=dburl,
+ session_info=system_session(),
+ credentials=creds, lp=lp)
+ except ldb.LdbError, (enum, estr):
+ logger.error("Unable to open sam database (%s) : %s" % \
+ (lp.samdb_url(), estr))
+ return 1
+
+ if os.path.exists(ldif_file):
+ logger.error("Specify a file (%s) that doesn't already exist." %
+ ldif_file)
+ return 1
+
+ try:
+ f = open(ldif_file, "w")
+ except (enum, estr):
+ logger.error("Unable to open (%s) : %s" % (ldif_file, estr))
+ return 1
+
+ try:
+ # Query Partitions
+ attrs = [ "objectClass",
+ "objectGUID",
+ "cn",
+ "whenChanged",
+ "objectSid",
+ "Enabled",
+ "systemFlags",
+ "dnsRoot",
+ "nCName",
+ "msDS-NC-Replica-Locations",
+ "msDS-NC-RO-Replica-Locations" ]
+
+ sstr = "CN=Partitions,%s" % self.samdb.get_config_basedn()
+ res = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
+ attrs=attrs,
+ expression="(objectClass=crossRef)")
+
+ # Write partitions output
+ write_search_result(self.samdb, f, res)
+
+ # Query cross reference container
+ attrs = [ "objectClass",
+ "objectGUID",
+ "cn",
+ "whenChanged",
+ "fSMORoleOwner",
+ "systemFlags",
+ "msDS-Behavior-Version",
+ "msDS-EnabledFeature" ]
+
+ sstr = "CN=Partitions,%s" % self.samdb.get_config_basedn()
+ res = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
+ attrs=attrs,
+ expression="(objectClass=crossRefContainer)")
+
+ # Write cross reference container output
+ write_search_result(self.samdb, f, res)
+
+ # Query Sites
+ attrs = [ "objectClass",
+ "objectGUID",
+ "cn",
+ "whenChanged",
+ "systemFlags" ]
+
+ sstr = "CN=Sites,%s" % self.samdb.get_config_basedn()
+ sites = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
+ attrs=attrs,
+ expression="(objectClass=site)")
+
+ # Write sites output
+ write_search_result(self.samdb, f, sites)
+
+ # Query NTDS Site Settings
+ for msg in sites:
+ sitestr = str(msg.dn)
+
+ attrs = [ "objectClass",
+ "objectGUID",
+ "cn",
+ "whenChanged",
+ "interSiteTopologyGenerator",
+ "interSiteTopologyFailover",
+ "schedule",
+ "options" ]
+
+ sstr = "CN=NTDS Site Settings,%s" % sitestr
+ res = self.samdb.search(base=sstr, scope=ldb.SCOPE_BASE,
+ attrs=attrs)
+
+ # Write Site Settings output
+ write_search_result(self.samdb, f, res)
+
+ # Naming context list
+ nclist = []
+
+ # Query Directory Service Agents
+ for msg in sites:
+ sstr = str(msg.dn)
+
+ ncattrs = [ "hasMasterNCs",
+ "msDS-hasMasterNCs",
+ "hasPartialReplicaNCs",
+ "msDS-HasDomainNCs",
+ "msDS-hasFullReplicaNCs",
+ "msDS-HasInstantiatedNCs" ]
+ attrs = [ "objectClass",
+ "objectGUID",
+ "cn",
+ "whenChanged",
+ "invocationID",
+ "options",
+ "msDS-isRODC",
+ "msDS-Behavior-Version" ]
+
+ res = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
+ attrs=attrs + ncattrs,
+ expression="(objectClass=nTDSDSA)")
+
+ # Spin thru all the DSAs looking for NC replicas
+ # and build a list of all possible Naming Contexts
+ # for subsequent retrieval below
+ for msg in res:
+ for k in msg.keys():
+ if k in ncattrs:
+ for value in msg[k]:
+ # Some of these have binary DNs so
+ # use dsdb_Dn to split out relevent parts
+ dsdn = dsdb_Dn(self.samdb, value)
+ dnstr = str(dsdn.dn)
+ if dnstr not in nclist:
+ nclist.append(dnstr)
+
+ # Write DSA output
+ write_search_result(self.samdb, f, res)
+
+ # Query NTDS Connections
+ for msg in sites:
+ sstr = str(msg.dn)
+
+ attrs = [ "objectClass",
+ "objectGUID",
+ "cn",
+ "whenChanged",
+ "options",
+ "whenCreated",
+ "enabledConnection",
+ "schedule",
+ "transportType",
+ "fromServer",
+ "systemFlags" ]
+
+ res = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
+ attrs=attrs,
+ expression="(objectClass=nTDSConnection)")
+ # Write NTDS Connection output
+ write_search_result(self.samdb, f, res)
+
+
+ # Query Intersite transports
+ attrs = [ "objectClass",
+ "objectGUID",
+ "cn",
+ "whenChanged",
+ "options",
+ "name",
+ "bridgeheadServerListBL",
+ "transportAddressAttribute" ]
+
+ sstr = "CN=Inter-Site Transports,CN=Sites,%s" % \
+ self.samdb.get_config_basedn()
+ res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
+ attrs=attrs,
+ expression="(objectClass=interSiteTransport)")
+
+ # Write inter-site transport output
+ write_search_result(self.samdb, f, res)
+
+ # Query siteLink
+ attrs = [ "objectClass",
+ "objectGUID",
+ "cn",
+ "whenChanged",
+ "systemFlags",
+ "options",
+ "schedule",
+ "replInterval",
+ "siteList",
+ "cost" ]
+
+ sstr = "CN=Sites,%s" % \
+ self.samdb.get_config_basedn()
+ res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
+ attrs=attrs,
+ expression="(objectClass=siteLink)")
+
+ # Write siteLink output
+ write_search_result(self.samdb, f, res)
+
+ # Query siteLinkBridge
+ attrs = [ "objectClass",
+ "objectGUID",
+ "cn",
+ "whenChanged",
+ "siteLinkList" ]
+
+ sstr = "CN=Sites,%s" % \
+ self.samdb.get_config_basedn()
+ res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
+ attrs=attrs,
+ expression="(objectClass=siteLinkBridge)")
+
+ # Write siteLinkBridge output
+ write_search_result(self.samdb, f, res)
+
+ # Query servers containers
+ # Needed for samdb.server_site_name()
+ attrs = [ "objectClass",
+ "objectGUID",
+ "cn",
+ "whenChanged",
+ "systemFlags" ]
+
+ sstr = "CN=Sites,%s" % self.samdb.get_config_basedn()
+ res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
+ attrs=attrs,
+ expression="(objectClass=serversContainer)")
+
+ # Write servers container output
+ write_search_result(self.samdb, f, res)
+
+ # Query servers
+ # Needed because some transport interfaces refer back to
+ # attributes found in the server object. Also needed
+ # so extended-dn will be happy with dsServiceName in rootDSE
+ attrs = [ "objectClass",
+ "objectGUID",
+ "cn",
+ "whenChanged",
+ "systemFlags",
+ "dNSHostName",
+ "mailAddress" ]
+
+ sstr = "CN=Sites,%s" % self.samdb.get_config_basedn()
+ res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
+ attrs=attrs,
+ expression="(objectClass=server)")
+
+ # Write server output
+ write_search_result(self.samdb, f, res)
+
+ # Query Naming Context replicas
+ attrs = [ "objectClass",
+ "objectGUID",
+ "cn",
+ "whenChanged",
+ "objectSid",
+ "fSMORoleOwner",
+ "msDS-Behavior-Version",
+ "repsFrom",
+ "repsTo" ]
+
+ for sstr in nclist:
+ res = self.samdb.search(sstr, scope=ldb.SCOPE_BASE,
+ attrs=attrs)
+
+ # Write naming context output
+ write_search_result(self.samdb, f, res)
+
+ # Query rootDSE replicas
+ attrs=[ "objectClass",
+ "objectGUID",
+ "cn",
+ "whenChanged",
+ "rootDomainNamingContext",
+ "configurationNamingContext",
+ "schemaNamingContext",
+ "defaultNamingContext",
+ "dsServiceName" ]
+
+ sstr = ""
+ res = self.samdb.search(sstr, scope=ldb.SCOPE_BASE,
+ attrs=attrs)
+
+ # Record the rootDSE object as a dn as it
+ # would appear in the base ldb file. We have
+ # to save it this way because we are going to
+ # be importing as an abbreviated database.
+ res[0].dn = ldb.Dn(self.samdb, "@ROOTDSE")
+
+ # Write rootdse output
+ write_search_result(self.samdb, f, res)
+
+ except ldb.LdbError, (enum, estr):
+ logger.error("Error processing (%s) : %s" % (sstr, estr))
+ return 1
+
+ f.close()
+ return 0
+
##################################################
# Global Functions
##################################################
def sort_replica_by_dsa_guid(rep1, rep2):
return cmp(rep1.rep_dsa_guid, rep2.rep_dsa_guid)
-def is_smtp_replication_availalbe():
+def sort_dsa_by_gc_and_guid(dsa1, dsa2):
+ if dsa1.is_gc() == True and dsa2.is_gc() == False:
+ return -1
+ if dsa1.is_gc() == False and dsa2.is_gc() == True:
+ return +1
+ return cmp(dsa1.dsa_guid, dsa2.dsa_guid)
+
+def is_smtp_replication_available():
"""Currently always returns false because Samba
- doesn't implement SMTP transfer for NC changes
- between DCs
+ doesn't implement SMTP transfer for NC changes
+ between DCs
"""
return False
+def write_search_result(samdb, f, res):
+ for msg in res:
+ lstr = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
+ f.write("%s" % lstr)
+ return
+
##################################################
# samba_kcc entry point
##################################################
@@ -1147,19 +2399,46 @@ parser.add_option_group(options.VersionOptions(parser))
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")
+
+parser.add_option("--debug", \
+ help="debug output", \
+ action="store_true")
+
+parser.add_option("--seed", \
+ help="random number seed", \
+ type=str, metavar="<number>")
+
+parser.add_option("--importldif", \
+ help="import topology ldif file", \
+ type=str, metavar="<file>")
+
+parser.add_option("--exportldif", \
+ help="export topology ldif file", \
+ type=str, metavar="<file>")
+
+parser.add_option("-H", "--URL" , \
+ help="LDB URL for database or target server", \
+ type=str, metavar="<URL>", dest="dburl")
+
+parser.add_option("--tmpdb", \
+ help="schemaless database file to create for ldif import", \
+ type=str, metavar="<file>")
logger = logging.getLogger("samba_kcc")
logger.addHandler(logging.StreamHandler(sys.stdout))
-lp = sambaopts.get_loadparm()
-creds = credopts.get_credentials(lp, fallback_machine=True)
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp, fallback_machine=True)
opts, args = parser.parse_args()
+if opts.readonly is None:
+ opts.readonly = False
+
if opts.debug:
logger.setLevel(logging.DEBUG)
+elif opts.readonly:
+ logger.setLevel(logging.INFO)
else:
logger.setLevel(logging.WARNING)
@@ -1169,18 +2448,24 @@ if opts.seed:
else:
random.seed(0xACE5CA11)
-private_dir = lp.get("private dir")
-samdb_path = os.path.join(private_dir, "samdb.ldb")
-
-try:
- samdb = SamDB(url=lp.samdb_url(), session_info=system_session(),
- credentials=creds, lp=lp)
-except ldb.LdbError, (num, msg):
- logger.info("Unable to open sam database %s : %s" % (lp.samdb_url(), msg))
- sys.exit(1)
+if opts.dburl is None:
+ opts.dburl = lp.samdb_url()
# Instantiate Knowledge Consistency Checker and perform run
-kcc = KCC(samdb)
-rc = kcc.run()
+kcc = KCC()
+
+if opts.exportldif:
+ rc = kcc.export_ldif(opts.dburl, lp, creds, opts.exportldif)
+ sys.exit(rc)
+
+if opts.importldif:
+ if opts.tmpdb is None or opts.tmpdb.startswith('ldap'):
+ logger.error("Specify a target temp database file with --tmpdb option.")
+ sys.exit(1)
+
+ rc = kcc.import_ldif(opts.tmpdb, lp, creds, opts.importldif)
+ if rc != 0:
+ sys.exit(rc)
+rc = kcc.run(opts.dburl, lp, creds)
sys.exit(rc)
diff --git a/source4/scripting/python/samba/kcc_utils.py b/source4/scripting/python/samba/kcc_utils.py
index f762f4a252..93096e9689 100644
--- a/source4/scripting/python/samba/kcc_utils.py
+++ b/source4/scripting/python/samba/kcc_utils.py
@@ -20,15 +20,14 @@
import ldb
import uuid
+import time
-from samba import dsdb
-from samba.dcerpc import (
- drsblobs,
- drsuapi,
- misc,
- )
+from samba import (dsdb, unix2nttime)
+from samba.dcerpc import (drsblobs, \
+ drsuapi, \
+ misc)
from samba.common import dsdb_Dn
-from samba.ndr import (ndr_unpack, ndr_pack)
+from samba.ndr import (ndr_unpack, ndr_pack)
class NCType(object):
@@ -42,47 +41,80 @@ class NamingContext(object):
Subclasses may inherit from this and specialize
"""
- def __init__(self, nc_dnstr, nc_guid=None, nc_sid=None):
+ def __init__(self, nc_dnstr):
"""Instantiate a NamingContext
:param nc_dnstr: NC dn 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_guid = None
+ self.nc_sid = None
+ self.nc_type = NCType.unknown
def __str__(self):
'''Debug dump string output of class'''
text = "%s:" % self.__class__.__name__
text = text + "\n\tnc_dnstr=%s" % self.nc_dnstr
text = text + "\n\tnc_guid=%s" % str(self.nc_guid)
- text = text + "\n\tnc_sid=%s" % self.nc_sid
+
+ if self.nc_sid is None:
+ text = text + "\n\tnc_sid=<absent>"
+ else:
+ text = text + "\n\tnc_sid=<present>"
+
text = text + "\n\tnc_type=%s" % self.nc_type
return text
+ def load_nc(self, samdb):
+ attrs = [ "objectGUID",
+ "objectSid" ]
+ try:
+ res = samdb.search(base=self.nc_dnstr,
+ scope=ldb.SCOPE_BASE, attrs=attrs)
+
+ except ldb.LdbError, (enum, estr):
+ raise Exception("Unable to find naming context (%s)" % \
+ (self.nc_dnstr, estr))
+ msg = res[0]
+ if "objectGUID" in msg:
+ self.nc_guid = misc.GUID(samdb.schema_format_value("objectGUID",
+ msg["objectGUID"][0]))
+ if "objectSid" in msg:
+ self.nc_sid = msg["objectSid"][0]
+
+ assert self.nc_guid is not None
+ return
+
def is_schema(self):
'''Return True if NC is schema'''
+ assert self.nc_type != NCType.unknown
return self.nc_type == NCType.schema
def is_domain(self):
'''Return True if NC is domain'''
+ assert self.nc_type != NCType.unknown
return self.nc_type == NCType.domain
def is_application(self):
'''Return True if NC is application'''
+ assert self.nc_type != NCType.unknown
return self.nc_type == NCType.application
def is_config(self):
'''Return True if NC is config'''
+ assert self.nc_type != NCType.unknown
return self.nc_type == NCType.config
def identify_by_basedn(self, samdb):
"""Given an NC object, identify what type is is thru
the samdb basedn strings and NC sid value
"""
+ # Invoke loader to initialize guid and more
+ # importantly sid value (sid is used to identify
+ # domain NCs)
+ if self.nc_guid is None:
+ self.load_nc(samdb)
+
# We check against schema and config because they
# will be the same for all nTDSDSAs in the forest.
# That leaves the domain NCs which can be identified
@@ -118,7 +150,7 @@ class NamingContext(object):
# NCs listed under hasMasterNCs are either
# default domain, schema, or config. We
- # utilize the identify_by_samdb_basedn() to
+ # utilize the identify_by_basedn() to
# identify those
elif attr == "hasMasterNCs":
self.identify_by_basedn(samdb)
@@ -136,14 +168,11 @@ class NCReplica(NamingContext):
class) and it identifies unique attributes of the DSA's replica for a NC.
"""
- def __init__(self, dsa_dnstr, dsa_guid, nc_dnstr,
- nc_guid=None, nc_sid=None):
+ def __init__(self, dsa_dnstr, dsa_guid, nc_dnstr):
"""Instantiate a Naming Context Replica
:param dsa_guid: GUID of DSA where replica appears
:param nc_dnstr: NC dn string
- :param nc_guid: NC guid
- :param nc_sid: NC sid
"""
self.rep_dsa_dnstr = dsa_dnstr
self.rep_dsa_guid = dsa_guid
@@ -152,6 +181,8 @@ class NCReplica(NamingContext):
self.rep_ro = False
self.rep_instantiated_flags = 0
+ self.rep_fsmo_role_owner = None
+
# RepsFromTo tuples
self.rep_repsFrom = []
@@ -163,17 +194,18 @@ class NCReplica(NamingContext):
self.rep_present_criteria_one = False
# Call my super class we inherited from
- NamingContext.__init__(self, nc_dnstr, nc_guid, nc_sid)
+ NamingContext.__init__(self, nc_dnstr)
def __str__(self):
'''Debug dump string output of class'''
text = "%s:" % self.__class__.__name__
- text = text + "\n\tdsa_dnstr=%s" % self.rep_dsa_dnstr
- text = text + "\n\tdsa_guid=%s" % str(self.rep_dsa_guid)
- text = text + "\n\tdefault=%s" % self.rep_default
- text = text + "\n\tro=%s" % self.rep_ro
- text = text + "\n\tpartial=%s" % self.rep_partial
- text = text + "\n\tpresent=%s" % self.is_present()
+ text = text + "\n\tdsa_dnstr=%s" % self.rep_dsa_dnstr
+ text = text + "\n\tdsa_guid=%s" % str(self.rep_dsa_guid)
+ text = text + "\n\tdefault=%s" % self.rep_default
+ text = text + "\n\tro=%s" % self.rep_ro
+ text = text + "\n\tpartial=%s" % self.rep_partial
+ text = text + "\n\tpresent=%s" % self.is_present()
+ text = text + "\n\tfsmo_role_owner=%s" % self.rep_fsmo_role_owner
for rep in self.rep_repsFrom:
text = text + "\n%s" % rep
@@ -283,7 +315,7 @@ class NCReplica(NamingContext):
ndr_unpack(drsblobs.repsFromToBlob, value))
self.rep_repsFrom.append(rep)
- def commit_repsFrom(self, samdb):
+ def commit_repsFrom(self, samdb, ro=False):
"""Commit repsFrom to the database"""
# XXX - This is not truly correct according to the MS-TECH
@@ -298,23 +330,39 @@ class NCReplica(NamingContext):
# older KCC also did
modify = False
newreps = []
+ delreps = []
for repsFrom in self.rep_repsFrom:
# Leave out any to be deleted from
- # replacement list
+ # replacement list. Build a list
+ # of to be deleted reps which we will
+ # remove from rep_repsFrom list below
if repsFrom.to_be_deleted == True:
+ delreps.append(repsFrom)
modify = True
continue
if repsFrom.is_modified():
+ repsFrom.set_unmodified()
modify = True
+ # current (unmodified) elements also get
+ # appended here but no changes will occur
+ # unless something is "to be modified" or
+ # "to be deleted"
newreps.append(ndr_pack(repsFrom.ndr_blob))
+ # Now delete these from our list of rep_repsFrom
+ for repsFrom in delreps:
+ self.rep_repsFrom.remove(repsFrom)
+ delreps = []
+
# Nothing to do if no reps have been modified or
- # need to be deleted. Leave database record "as is"
- if modify == False:
+ # need to be deleted or input option has informed
+ # us to be "readonly" (ro). Leave database
+ # record "as is"
+ if modify == False or ro == True:
return
m = ldb.Message()
@@ -330,15 +378,51 @@ class NCReplica(NamingContext):
raise Exception("Could not set repsFrom for (%s) - (%s)" %
(self.dsa_dnstr, estr))
+ def dumpstr_to_be_deleted(self):
+ text=""
+ for repsFrom in self.rep_repsFrom:
+ if repsFrom.to_be_deleted == True:
+ if text:
+ text = text + "\n%s" % repsFrom
+ else:
+ text = "%s" % repsFrom
+ return text
+
+ def dumpstr_to_be_modified(self):
+ text=""
+ for repsFrom in self.rep_repsFrom:
+ if repsFrom.is_modified() == True:
+ if text:
+ text = text + "\n%s" % repsFrom
+ else:
+ text = "%s" % repsFrom
+ return text
+
def load_fsmo_roles(self, samdb):
- # XXX - to be implemented
+ """Given an NC replica which has been discovered thru the nTDSDSA
+ database object, load the fSMORoleOwner attribute.
+ """
+ try:
+ res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
+ attrs=[ "fSMORoleOwner" ])
+
+ except ldb.LdbError, (enum, estr):
+ raise Exception("Unable to find NC for (%s) - (%s)" %
+ (self.nc_dnstr, estr))
+
+ msg = res[0]
+
+ # Possibly no fSMORoleOwner
+ if "fSMORoleOwner" in msg:
+ self.rep_fsmo_role_owner = msg["fSMORoleOwner"]
return
def is_fsmo_role_owner(self, dsa_dnstr):
- # XXX - to be implemented
+ if self.rep_fsmo_role_owner is not None and \
+ self.rep_fsmo_role_owner == dsa_dnstr:
+ return True
return False
-
class DirectoryServiceAgent(object):
def __init__(self, dsa_dnstr):
@@ -352,6 +436,7 @@ class DirectoryServiceAgent(object):
self.dsa_guid = None
self.dsa_ivid = None
self.dsa_is_ro = False
+ self.dsa_is_istg = False
self.dsa_options = 0
self.dsa_behavior = 0
self.default_dnstr = None # default domain dn string for dsa
@@ -365,7 +450,7 @@ class DirectoryServiceAgent(object):
self.needed_rep_table = {}
# NTDSConnections for this dsa. These are current
- # valid connections that are committed or "to be committed"
+ # valid connections that are committed or pending a commit
# in the database. Indexed by DN string of connection
self.connect_table = {}
@@ -382,6 +467,7 @@ class DirectoryServiceAgent(object):
text = text + "\n\tro=%s" % self.is_ro()
text = text + "\n\tgc=%s" % self.is_gc()
+ text = text + "\n\tistg=%s" % self.is_istg()
text = text + "\ncurrent_replica_table:"
text = text + "\n%s" % self.dumpstr_current_replica_table()
@@ -393,7 +479,15 @@ class DirectoryServiceAgent(object):
return text
def get_current_replica(self, nc_dnstr):
- return self.current_rep_table[nc_dnstr]
+ if nc_dnstr in self.current_rep_table.keys():
+ return self.current_rep_table[nc_dnstr]
+ else:
+ return None
+
+ def is_istg(self):
+ '''Returns True if dsa is intersite topology generator for it's site'''
+ # The KCC on an RODC always acts as an ISTG for itself
+ return self.dsa_is_istg or self.dsa_is_ro
def is_ro(self):
'''Returns True if dsa a read only domain controller'''
@@ -415,11 +509,11 @@ class DirectoryServiceAgent(object):
return True
return False
- def should_translate_ntdsconn(self):
+ def is_translate_ntdsconn_disabled(self):
"""Whether this allows NTDSConnection translation in its options."""
if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0:
- return False
- return True
+ return True
+ return False
def get_rep_tables(self):
"""Return DSA current and needed replica tables
@@ -433,12 +527,11 @@ class DirectoryServiceAgent(object):
def load_dsa(self, samdb):
"""Load a DSA from the samdb.
-
+
Prior initialization has given us the DN of the DSA that we are to
load. This method initializes all other attributes, including loading
- the NC replica table for this DSA.
+ the NC replica table for this DSA.
"""
- controls = [ "extended_dn:1:1" ]
attrs = ["objectGUID",
"invocationID",
"options",
@@ -446,7 +539,7 @@ class DirectoryServiceAgent(object):
"msDS-Behavior-Version"]
try:
res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
- attrs=attrs, controls=controls)
+ attrs=attrs)
except ldb.LdbError, (enum, estr):
raise Exception("Unable to find nTDSDSA for (%s) - (%s)" %
@@ -481,17 +574,16 @@ class DirectoryServiceAgent(object):
def load_current_replica_table(self, samdb):
"""Method to load the NC replica's listed for DSA object.
-
+
This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs,
hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, and
msDS-HasInstantiatedNCs) to determine complete list of NC replicas that
are enumerated for the DSA. Once a NC replica is loaded it is
identified (schema, config, etc) and the other replica attributes
- (partial, ro, etc) are determined.
+ (partial, ro, etc) are determined.
:param samdb: database to query for DSA replica list
"""
- controls = ["extended_dn:1:1"]
ncattrs = [ # not RODC - default, config, schema (old style)
"hasMasterNCs",
# not RODC - default, config, schema, app NCs
@@ -506,7 +598,7 @@ class DirectoryServiceAgent(object):
"msDS-HasInstantiatedNCs" ]
try:
res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
- attrs=ncattrs, controls=controls)
+ attrs=ncattrs)
except ldb.LdbError, (enum, estr):
raise Exception("Unable to find nTDSDSA NCs for (%s) - (%s)" %
@@ -533,23 +625,13 @@ class DirectoryServiceAgent(object):
# listed.
for value in res[0][k]:
# Turn dn into a dsdb_Dn so we can use
- # its methods to parse the extended pieces.
- # Note we don't really need the exact sid value
- # but instead only need to know if its present.
- dsdn = dsdb_Dn(samdb, value)
- guid = dsdn.dn.get_extended_component('GUID')
- sid = dsdn.dn.get_extended_component('SID')
+ # its methods to parse a binary DN
+ dsdn = dsdb_Dn(samdb, value)
flags = dsdn.get_binary_integer()
dnstr = str(dsdn.dn)
- if guid is None:
- raise Exception("Missing GUID for (%s) - (%s: %s)" %
- (self.dsa_dnstr, k, value))
- guid = misc.GUID(guid)
-
- if not dnstr in tmp_table:
- rep = NCReplica(self.dsa_dnstr, self.dsa_guid,
- dnstr, guid, sid)
+ if not dnstr in tmp_table.keys():
+ rep = NCReplica(self.dsa_dnstr, self.dsa_guid, dnstr)
tmp_table[dnstr] = rep
else:
rep = tmp_table[dnstr]
@@ -572,7 +654,7 @@ class DirectoryServiceAgent(object):
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
+ 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
@@ -603,23 +685,45 @@ class DirectoryServiceAgent(object):
connect.load_connection(samdb)
self.connect_table[dnstr] = connect
- def commit_connection_table(self, samdb):
+ def commit_connections(self, samdb, ro=False):
"""Method to commit any uncommitted nTDSConnections
- 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
+ modifications that are in our table. These would be
+ identified connections that are marked to be added or
+ deleted
+
+ :param samdb: database to commit DSA connection list to
+ :param ro: if (true) then peform internal operations but
+ do not write to the database (readonly)
"""
+ delconn = []
+
for dnstr, connect in self.connect_table.items():
- connect.commit_connection(samdb)
+ if connect.to_be_added:
+ connect.commit_added(samdb, ro)
+
+ if connect.to_be_modified:
+ connect.commit_modified(samdb, ro)
+
+ if connect.to_be_deleted:
+ connect.commit_deleted(samdb, ro)
+ delconn.append(dnstr)
+
+ # Now delete the connection from the table
+ for dnstr in delconn:
+ del self.connect_table[dnstr]
+
+ return
def add_connection(self, dnstr, connect):
+ assert dnstr not in self.connect_table.keys()
self.connect_table[dnstr] = connect
def get_connection_by_from_dnstr(self, from_dnstr):
"""Scan DSA nTDSConnection table and return connection
- with a "fromServer" dn string equivalent to method
- input parameter.
- :param from_dnstr: search for this from server entry
+ with a "fromServer" dn string equivalent to method
+ input parameter.
+
+ :param from_dnstr: search for this from server entry
"""
for dnstr, connect in self.connect_table.items():
if connect.get_from_dnstr() == from_dnstr:
@@ -656,20 +760,71 @@ class DirectoryServiceAgent(object):
text = "%s" % self.connect_table[k]
return text
+ def new_connection(self, options, flags, transport, from_dnstr, sched):
+ """Set up a new connection for the DSA based on input
+ parameters. Connection will be added to the DSA
+ connect_table and will be marked as "to be added" pending
+ a call to commit_connections()
+ """
+ dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
+
+ connect = NTDSConnection(dnstr)
+ connect.to_be_added = True
+ connect.enabled = True
+ connect.from_dnstr = from_dnstr
+ connect.options = options
+ connect.flags = flags
+
+ if transport is not None:
+ connect.transport_dnstr = transport.dnstr
+
+ if sched is not None:
+ connect.schedule = sched
+ else:
+ # Create schedule. Attribute valuse set according to MS-TECH
+ # intrasite connection creation document
+ connect.schedule = drsblobs.schedule()
+
+ connect.schedule.size = 188
+ connect.schedule.bandwidth = 0
+ connect.schedule.numberOfSchedules = 1
+
+ header = drsblobs.scheduleHeader()
+ header.type = 0
+ header.offset = 20
+
+ connect.schedule.headerArray = [ header ]
+
+ # 168 byte instances of the 0x01 value. The low order 4 bits
+ # of the byte equate to 15 minute intervals within a single hour.
+ # There are 168 bytes because there are 168 hours in a full week
+ # Effectively we are saying to perform replication at the end of
+ # each hour of the week
+ data = drsblobs.scheduleSlots()
+ data.slots = [ 0x01 ] * 168
+
+ connect.schedule.dataArray = [ data ]
+
+ self.add_connection(dnstr, connect);
+ return connect
+
class NTDSConnection(object):
"""Class defines a nTDSConnection found under a DSA
"""
def __init__(self, dnstr):
self.dnstr = dnstr
+ self.guid = None
self.enabled = False
- self.committed = False # new connection needs to be committed
+ self.whenCreated = 0
+ self.to_be_added = False # new connection needs to be added
+ self.to_be_deleted = False # old connection needs to be deleted
+ self.to_be_modified = False
self.options = 0
- self.flags = 0
+ self.system_flags = 0
self.transport_dnstr = None
self.transport_guid = None
self.from_dnstr = None
- self.from_guid = None
self.schedule = None
def __str__(self):
@@ -677,16 +832,21 @@ class NTDSConnection(object):
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\tto_be_added=%s" % self.to_be_added
+ text = text + "\n\tto_be_deleted=%s" % self.to_be_deleted
+ text = text + "\n\tto_be_modified=%s" % self.to_be_modified
text = text + "\n\toptions=0x%08X" % self.options
- text = text + "\n\tflags=0x%08X" % self.flags
+ text = text + "\n\tsystem_flags=0x%08X" % self.system_flags
+ text = text + "\n\twhenCreated=%d" % self.whenCreated
text = text + "\n\ttransport_dn=%s" % self.transport_dnstr
+ if self.guid is not None:
+ text = text + "\n\tguid=%s" % str(self.guid)
+
if self.transport_guid is not None:
text = text + "\n\ttransport_guid=%s" % str(self.transport_guid)
text = text + "\n\tfrom_dn=%s" % self.from_dnstr
- 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
@@ -708,19 +868,20 @@ class NTDSConnection(object):
def load_connection(self, samdb):
"""Given a NTDSConnection object with an prior initialization
- for the object's DN, search for the DN and load attributes
- from the samdb.
+ for the object's DN, search for the DN and load attributes
+ from the samdb.
"""
- controls = ["extended_dn:1:1"]
attrs = [ "options",
"enabledConnection",
"schedule",
+ "whenCreated",
+ "objectGUID",
"transportType",
"fromServer",
"systemFlags" ]
try:
res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
- attrs=attrs, controls=controls)
+ attrs=attrs)
except ldb.LdbError, (enum, estr):
raise Exception("Unable to find nTDSConnection for (%s) - (%s)" %
@@ -730,59 +891,105 @@ class NTDSConnection(object):
if "options" in msg:
self.options = int(msg["options"][0])
+
if "enabledConnection" in msg:
if msg["enabledConnection"][0].upper().lstrip().rstrip() == "TRUE":
self.enabled = True
+
if "systemFlags" in msg:
- self.flags = int(msg["systemFlags"][0])
- if "transportType" in msg:
- dsdn = dsdb_Dn(samdb, msg["tranportType"][0])
- guid = dsdn.dn.get_extended_component('GUID')
+ self.system_flags = int(msg["systemFlags"][0])
- assert guid is not None
- self.transport_guid = misc.GUID(guid)
+ if "objectGUID" in msg:
+ self.guid = \
+ misc.GUID(samdb.schema_format_value("objectGUID",
+ msg["objectGUID"][0]))
- self.transport_dnstr = str(dsdn.dn)
- assert self.transport_dnstr is not None
+ if "transportType" in msg:
+ dsdn = dsdb_Dn(samdb, msg["tranportType"][0])
+ self.load_connection_transport(str(dsdn.dn))
if "schedule" in msg:
self.schedule = ndr_unpack(drsblobs.replSchedule, msg["schedule"][0])
+ if "whenCreated" in msg:
+ self.whenCreated = ldb.string_to_time(msg["whenCreated"][0])
+
if "fromServer" in msg:
dsdn = dsdb_Dn(samdb, msg["fromServer"][0])
- 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 is not None
- # Was loaded from database so connection is currently committed
- self.committed = True
+ def load_connection_transport(self, tdnstr):
+ """Given a NTDSConnection object which enumerates a transport
+ DN, load the transport information for the connection object
+
+ :param tdnstr: transport DN to load
+ """
+ attrs = [ "objectGUID" ]
+ try:
+ res = samdb.search(base=tdnstr,
+ scope=ldb.SCOPE_BASE, attrs=attrs)
- def commit_connection(self, samdb):
- """Given a NTDSConnection object that is not committed in the
- sam database, perform a commit action.
+ except ldb.LdbError, (enum, estr):
+ raise Exception("Unable to find transport (%s)" %
+ (tdnstr, estr))
+
+ if "objectGUID" in res[0]:
+ self.transport_dnstr = tdnstr
+ self.transport_guid = \
+ misc.GUID(samdb.schema_format_value("objectGUID",
+ msg["objectGUID"][0]))
+ assert self.transport_dnstr is not None
+ assert self.transport_guid is not None
+ return
+
+ def commit_deleted(self, samdb, ro=False):
+ """Local helper routine for commit_connections() which
+ handles committed connections that are to be deleted from
+ the database database
"""
- # nothing to do
- if self.committed == True:
+ assert self.to_be_deleted
+ self.to_be_deleted = False
+
+ # No database modification requested
+ if ro == True:
+ return
+
+ try:
+ samdb.delete(self.dnstr)
+ except ldb.LdbError, (enum, estr):
+ raise Exception("Could not delete nTDSConnection for (%s) - (%s)" % \
+ (self.dnstr, estr))
+
+ return
+
+ def commit_added(self, samdb, ro=False):
+ """Local helper routine for commit_connections() which
+ handles committed connections that are to be added to the
+ database
+ """
+ assert self.to_be_added
+ self.to_be_added = False
+
+ # No database modification requested
+ if ro == True:
return
# First verify we don't have this entry to ensure nothing
# is programatically amiss
+ found = False
try:
msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
- found = True
+ if len(msg) != 0:
+ 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)" %
+ if enum != ldb.ERR_NO_SUCH_OBJECT:
+ raise Exception("Unable to search for (%s) - (%s)" % \
(self.dnstr, estr))
if found:
- raise Exception("nTDSConnection for (%s) already exists!" % self.dnstr)
+ raise Exception("nTDSConnection for (%s) already exists!" % \
+ self.dnstr)
if self.enabled:
enablestr = "TRUE"
@@ -806,7 +1013,13 @@ class NTDSConnection(object):
m["options"] = \
ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options")
m["systemFlags"] = \
- ldb.MessageElement(str(self.flags), ldb.FLAG_MOD_ADD, "systemFlags")
+ ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_ADD, \
+ "systemFlags")
+
+ if self.transport_dnstr is not None:
+ m["transportType"] = \
+ ldb.MessageElement(str(self.transport_dnstr), ldb.FLAG_MOD_ADD, \
+ "transportType")
if self.schedule is not None:
m["schedule"] = \
@@ -817,11 +1030,97 @@ class NTDSConnection(object):
except ldb.LdbError, (enum, estr):
raise Exception("Could not add nTDSConnection for (%s) - (%s)" % \
(self.dnstr, estr))
- self.committed = True
+ return
+
+ def commit_modified(self, samdb, ro=False):
+ """Local helper routine for commit_connections() which
+ handles committed connections that are to be modified to the
+ database
+ """
+ assert self.to_be_modified
+ self.to_be_modified = False
+
+ # No database modification requested
+ if ro == True:
+ return
+
+ # First verify we have this entry to ensure nothing
+ # is programatically amiss
+ try:
+ msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
+ found = True
+
+ except ldb.LdbError, (enum, estr):
+ if enum == ldb.ERR_NO_SUCH_OBJECT:
+ found = False
+ else:
+ raise Exception("Unable to search for (%s) - (%s)" % \
+ (self.dnstr, estr))
+ if found == False:
+ raise Exception("nTDSConnection for (%s) doesn't exist!" % \
+ self.dnstr)
+
+ if self.enabled:
+ enablestr = "TRUE"
+ else:
+ enablestr = "FALSE"
+
+ # Prepare a message for modifying the samdb
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, self.dnstr)
+
+ m["enabledConnection"] = \
+ ldb.MessageElement(enablestr, ldb.FLAG_MOD_REPLACE, \
+ "enabledConnection")
+ m["fromServer"] = \
+ ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_REPLACE, \
+ "fromServer")
+ m["options"] = \
+ ldb.MessageElement(str(self.options), ldb.FLAG_MOD_REPLACE, \
+ "options")
+ m["systemFlags"] = \
+ ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_REPLACE, \
+ "systemFlags")
+
+ if self.transport_dnstr is not None:
+ m["transportType"] = \
+ ldb.MessageElement(str(self.transport_dnstr), \
+ ldb.FLAG_MOD_REPLACE, "transportType")
+ else:
+ m["transportType"] = \
+ ldb.MessageElement([], \
+ ldb.FLAG_MOD_DELETE, "transportType")
+
+ if self.schedule is not None:
+ m["schedule"] = \
+ ldb.MessageElement(ndr_pack(self.schedule), \
+ ldb.FLAG_MOD_REPLACE, "schedule")
+ else:
+ m["schedule"] = \
+ ldb.MessageElement([], \
+ ldb.FLAG_MOD_DELETE, "schedule")
+ try:
+ samdb.modify(m)
+ except ldb.LdbError, (enum, estr):
+ raise Exception("Could not modify nTDSConnection for (%s) - (%s)" % \
+ (self.dnstr, estr))
+ return
+
+ def set_modified(self, truefalse):
+ self.to_be_modified = truefalse
+ return
+
+ def set_added(self, truefalse):
+ self.to_be_added = truefalse
+ return
+
+ def set_deleted(self, truefalse):
+ self.to_be_deleted = truefalse
+ 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
+ replication interval within the week. False otherwise
"""
if self.schedule is None or self.schedule.dataArray[0] is None:
return False
@@ -831,19 +1130,52 @@ class NTDSConnection(object):
return True
return False
+ def is_equivalent_schedule(self, sched):
+ """Returns True if our schedule is equivalent to the input
+ comparison schedule.
+
+ :param shed: schedule to compare to
+ """
+ if self.schedule is not None:
+ if sched is None:
+ return False
+ elif sched is None:
+ return True
+
+ if self.schedule.size != sched.size or \
+ self.schedule.bandwidth != sched.bandwidth or \
+ self.schedule.numberOfSchedules != sched.numberOfSchedules:
+ return False
+
+ for i, header in enumerate(self.schedule.headerArray):
+
+ if self.schedule.headerArray[i].type != sched.headerArray[i].type:
+ return False
+
+ if self.schedule.headerArray[i].offset != \
+ sched.headerArray[i].offset:
+ return False
+
+ for a, b in zip(self.schedule.dataArray[i].slots, \
+ sched.dataArray[i].slots):
+ if a != b:
+ return False
+ return True
+
def convert_schedule_to_repltimes(self):
"""Convert NTDS Connection schedule to replTime schedule.
- NTDS Connection schedule slots are double the size of
- the replTime slots but the top portion of the NTDS
- Connection schedule slot (4 most significant bits in
- uchar) are unused. The 4 least significant bits have
- the same (15 minute interval) bit positions as replTimes.
- We thus pack two elements of the NTDS Connection schedule
- slots into one element of the replTimes slot
- If no schedule appears in NTDS Connection then a default
- of 0x11 is set in each replTimes slot as per behaviour
- noted in a Windows DC. That default would cause replication
- within the last 15 minutes of each hour.
+
+ 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
@@ -856,12 +1188,56 @@ class NTDSConnection(object):
def is_rodc_topology(self):
"""Returns True if NTDS Connection specifies RODC
- topology only
+ topology only
"""
if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
return False
return True
+ def is_generated(self):
+ """Returns True if NTDS Connection was generated by the
+ KCC topology algorithm as opposed to set by the administrator
+ """
+ if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0:
+ return False
+ return True
+
+ def is_override_notify_default(self):
+ """Returns True if NTDS Connection should override notify default
+ """
+ if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0:
+ return False
+ return True
+
+ def is_use_notify(self):
+ """Returns True if NTDS Connection should use notify
+ """
+ if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0:
+ return False
+ return True
+
+ def is_twoway_sync(self):
+ """Returns True if NTDS Connection should use twoway sync
+ """
+ if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0:
+ return False
+ return True
+
+ def is_intersite_compression_disabled(self):
+ """Returns True if NTDS Connection intersite compression
+ is disabled
+ """
+ if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0:
+ return False
+ return True
+
+ def is_user_owned_schedule(self):
+ """Returns True if NTDS Connection has a user owned schedule
+ """
+ if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0:
+ return False
+ return True
+
def is_enabled(self):
"""Returns True if NTDS Connection is enabled
"""
@@ -881,6 +1257,8 @@ class Partition(NamingContext):
"""
def __init__(self, partstr):
self.partstr = partstr
+ self.enabled = True
+ self.system_flags = 0
self.rw_location_list = []
self.ro_location_list = []
@@ -899,13 +1277,14 @@ class Partition(NamingContext):
:param samdb: sam database to load partition from
"""
- controls = ["extended_dn:1:1"]
attrs = [ "nCName",
+ "Enabled",
+ "systemFlags",
"msDS-NC-Replica-Locations",
"msDS-NC-RO-Replica-Locations" ]
try:
res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE,
- attrs=attrs, controls=controls)
+ attrs=attrs)
except ldb.LdbError, (enum, estr):
raise Exception("Unable to find partition for (%s) - (%s)" % (
@@ -916,43 +1295,57 @@ class Partition(NamingContext):
if k == "dn":
continue
+ if k == "Enabled":
+ if msg[k][0].upper().lstrip().rstrip() == "TRUE":
+ self.enabled = True
+ else:
+ self.enabled = False
+ continue
+
+ if k == "systemFlags":
+ self.system_flags = int(msg[k][0])
+ continue
+
for value in msg[k]:
- # Turn dn into a dsdb_Dn so we can use
- # its methods to parse the extended pieces.
- # Note we don't really need the exact sid value
- # but instead only need to know if its present.
- dsdn = dsdb_Dn(samdb, value)
- guid = dsdn.dn.get_extended_component('GUID')
- sid = dsdn.dn.get_extended_component('SID')
-
- if guid is None:
- raise Exception("Missing GUID for (%s) - (%s: %s)" % \
- (self.partstr, k, value))
- guid = misc.GUID(guid)
+ dsdn = dsdb_Dn(samdb, value)
+ dnstr = str(dsdn.dn)
if k == "nCName":
- self.nc_dnstr = str(dsdn.dn)
- self.nc_guid = guid
- self.nc_sid = sid
+ self.nc_dnstr = dnstr
continue
if k == "msDS-NC-Replica-Locations":
- self.rw_location_list.append(str(dsdn.dn))
+ self.rw_location_list.append(dnstr)
continue
if k == "msDS-NC-RO-Replica-Locations":
- self.ro_location_list.append(str(dsdn.dn))
+ self.ro_location_list.append(dnstr)
continue
# Now identify what type of NC this partition
# enumerated
self.identify_by_basedn(samdb)
+ def is_enabled(self):
+ """Returns True if partition is enabled
+ """
+ return self.is_enabled
+
+ def is_foreign(self):
+ """Returns True if this is not an Active Directory NC in our
+ forest but is instead something else (e.g. a foreign NC)
+ """
+ if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0:
+ return True
+ else:
+ return False
+
def should_be_present(self, target_dsa):
"""Tests whether this partition should have an NC replica
- on the target dsa. This method returns a tuple of
- needed=True/False, ro=True/False, partial=True/False
- :param target_dsa: should NC be present on target dsa
+ on the target dsa. This method returns a tuple of
+ needed=True/False, ro=True/False, partial=True/False
+
+ :param target_dsa: should NC be present on target dsa
"""
needed = False
ro = False
@@ -1009,19 +1402,28 @@ class Partition(NamingContext):
class Site(object):
-
+ """An individual site object discovered thru the configuration
+ naming context. Contains all DSAs that exist within the site
+ """
def __init__(self, site_dnstr):
- self.site_dnstr = site_dnstr
- self.site_options = 0
- self.dsa_table = {}
+ self.site_dnstr = site_dnstr
+ self.site_options = 0
+ self.site_topo_generator = None
+ self.site_topo_failover = 0 # appears to be in minutes
+ self.dsa_table = {}
def load_site(self, samdb):
"""Loads the NTDS Site Settions options attribute for the site
+ as well as querying and loading all DSAs that appear within
+ the site.
"""
ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
+ attrs = ["options",
+ "interSiteTopologyFailover",
+ "interSiteTopologyGenerator"]
try:
res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE,
- attrs=["options"])
+ attrs=attrs)
except ldb.LdbError, (enum, estr):
raise Exception("Unable to find site settings for (%s) - (%s)" %
(ssdn, estr))
@@ -1030,12 +1432,18 @@ class Site(object):
if "options" in msg:
self.site_options = int(msg["options"][0])
+ if "interSiteTopologyGenerator" in msg:
+ self.site_topo_generator = str(msg["interSiteTopologyGenerator"][0])
+
+ if "interSiteTopologyFailover" in msg:
+ self.site_topo_failover = int(msg["interSiteTopologyFailover"][0])
+
self.load_all_dsa(samdb)
def load_all_dsa(self, samdb):
"""Discover all nTDSDSA thru the sites entry and
- instantiate and load the DSAs. Each dsa is inserted
- into the dsa_table by dn string.
+ instantiate and load the DSAs. Each dsa is inserted
+ into the dsa_table by dn string.
"""
try:
res = samdb.search(self.site_dnstr,
@@ -1067,7 +1475,7 @@ class Site(object):
def get_dsa(self, dnstr):
"""Return a previously loaded DSA object by consulting
- the sites dsa_table for the provided DSA dn string
+ the sites dsa_table for the provided DSA dn string
:return: None if DSA doesn't exist
"""
@@ -1075,25 +1483,222 @@ class Site(object):
return self.dsa_table[dnstr]
return None
+ def select_istg(self, samdb, mydsa, ro):
+ """Determine if my DC should be an intersite topology
+ generator. If my DC is the istg and is both a writeable
+ DC and the database is opened in write mode then we perform
+ an originating update to set the interSiteTopologyGenerator
+ attribute in the NTDS Site Settings object. An RODC always
+ acts as an ISTG for itself.
+ """
+ # The KCC on an RODC always acts as an ISTG for itself
+ if mydsa.dsa_is_ro:
+ mydsa.dsa_is_istg = True
+ return True
+
+ # Find configuration NC replica for my DSA
+ for c_rep in mydsa.current_rep_table.values():
+ if c_rep.is_config():
+ break
+
+ if c_rep is None:
+ raise Exception("Unable to find config NC replica for (%s)" % \
+ mydsa.dsa_dnstr)
+
+ # Load repsFrom if not already loaded so we can get the current
+ # state of the config replica and whether we are getting updates
+ # from the istg
+ c_rep.load_repsFrom(samdb)
+
+ # From MS-Tech ISTG selection:
+ # First, the KCC on a writable DC determines whether it acts
+ # as an ISTG for its site
+ #
+ # Let s be the object such that s!lDAPDisplayName = nTDSDSA
+ # and classSchema in s!objectClass.
+ #
+ # Let D be the sequence of objects o in the site of the local
+ # DC such that o!objectCategory = s. D is sorted in ascending
+ # order by objectGUID.
+ #
+ # Which is a fancy way of saying "sort all the nTDSDSA objects
+ # in the site by guid in ascending order". Place sorted list
+ # in D_sort[]
+ D_sort = []
+ d_dsa = None
+
+ unixnow = int(time.time()) # seconds since 1970
+ ntnow = unix2nttime(unixnow) # double word number of 100 nanosecond
+ # intervals since 1600s
+
+ for dsa in self.dsa_table.values():
+ D_sort.append(dsa)
+
+ D_sort.sort(sort_dsa_by_guid)
+
+ # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
+ # if o!interSiteTopologyFailover is 0 or has no value.
+ #
+ # Note: lastSuccess and ntnow are in 100 nanosecond intervals
+ # so it appears we have to turn f into the same interval
+ #
+ # interSiteTopologyFailover (if set) appears to be in minutes
+ # so we'll need to convert to senconds and then 100 nanosecond
+ # intervals
+ #
+ # 10,000,000 is number of 100 nanosecond intervals in a second
+ if self.site_topo_failover == 0:
+ f = 2 * 60 * 60 * 10000000
+ else:
+ f = self.site_topo_failover * 60 * 10000000
+
+ # From MS-Tech ISTG selection:
+ # If o != NULL and o!interSiteTopologyGenerator is not the
+ # nTDSDSA object for the local DC and
+ # o!interSiteTopologyGenerator is an element dj of sequence D:
+ #
+ if self.site_topo_generator is not None and \
+ self.site_topo_generator in self.dsa_table.keys():
+ d_dsa = self.dsa_table[self.site_topo_generator]
+ j_idx = D_sort.index(d_dsa)
+
+ if d_dsa is not None and d_dsa is not mydsa:
+ # From MS-Tech ISTG selection:
+ # Let c be the cursor in the replUpToDateVector variable
+ # associated with the NC replica of the config NC such
+ # that c.uuidDsa = dj!invocationId. If no such c exists
+ # (No evidence of replication from current ITSG):
+ # Let i = j.
+ # Let t = 0.
+ #
+ # Else if the current time < c.timeLastSyncSuccess - f
+ # (Evidence of time sync problem on current ISTG):
+ # Let i = 0.
+ # Let t = 0.
+ #
+ # Else (Evidence of replication from current ITSG):
+ # Let i = j.
+ # Let t = c.timeLastSyncSuccess.
+ #
+ # last_success appears to be a double word containing
+ # number of 100 nanosecond intervals since the 1600s
+ if d_dsa.dsa_ivid != c_rep.source_dsa_invocation_id:
+ i_idx = j_idx
+ t_time = 0
+
+ elif ntnow < (c_rep.last_success - f):
+ i_idx = 0
+ t_time = 0
+
+ else:
+ i_idx = j_idx
+ t_time = c_rep.last_success
+
+ # Otherwise (Nominate local DC as ISTG):
+ # Let i be the integer such that di is the nTDSDSA
+ # object for the local DC.
+ # Let t = the current time.
+ else:
+ i_idx = D_sort.index(mydsa)
+ t_time = ntnow
+
+ # Compute a function that maintains the current ISTG if
+ # it is alive, cycles through other candidates if not.
+ #
+ # Let k be the integer (i + ((current time - t) /
+ # o!interSiteTopologyFailover)) MOD |D|.
+ #
+ # Note: We don't want to divide by zero here so they must
+ # have meant "f" instead of "o!interSiteTopologyFailover"
+ k_idx = (i_idx + ((ntnow - t_time) / f)) % len(D_sort)
+
+ # The local writable DC acts as an ISTG for its site if and
+ # only if dk is the nTDSDSA object for the local DC. If the
+ # local DC does not act as an ISTG, the KCC skips the
+ # remainder of this task.
+ d_dsa = D_sort[k_idx]
+ d_dsa.dsa_is_istg = True
+
+ # Update if we are the ISTG, otherwise return
+ if d_dsa is not mydsa:
+ return False
+
+ # Nothing to do
+ if self.site_topo_generator == mydsa.dsa_dnstr:
+ return True
+
+ self.site_topo_generator = mydsa.dsa_dnstr
+
+ # If readonly database then do not perform a
+ # persistent update
+ if ro == True:
+ return True
+
+ # Perform update to the samdb
+ ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, ssdn)
+
+ m["interSiteTopologyGenerator"] = \
+ ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE, \
+ "interSiteTopologyGenerator")
+ try:
+ samdb.modify(m)
+
+ except ldb.LdbError, estr:
+ raise Exception("Could not set interSiteTopologyGenerator for (%s) - (%s)" %
+ (ssdn, estr))
+ return True
+
def is_intrasite_topology_disabled(self):
- '''Returns True if intrasite topology is disabled for site'''
+ '''Returns True if intra-site topology is disabled for site'''
if (self.site_options &
dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0:
return True
return False
- def should_detect_stale(self):
- '''Returns True if detect stale is enabled for site'''
+ def is_intersite_topology_disabled(self):
+ '''Returns True if inter-site topology is disabled for site'''
+ if (self.site_options &
+ dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED) != 0:
+ return True
+ return False
+
+ def is_random_bridgehead_disabled(self):
+ '''Returns True if selection of random bridgehead is disabled'''
if (self.site_options &
- dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) == 0:
+ dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0:
return True
return False
+ def is_detect_stale_disabled(self):
+ '''Returns True if detect stale is disabled for site'''
+ if (self.site_options &
+ dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0:
+ return True
+ return False
+
+ def is_cleanup_ntdsconn_disabled(self):
+ '''Returns True if NTDS Connection cleanup is disabled for site'''
+ if (self.site_options &
+ dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0:
+ return True
+ return False
+
+ def same_site(self, dsa):
+ '''Return True if dsa is in this site'''
+ if self.get_dsa(dsa.dsa_dnstr):
+ return True
+ return False
+
def __str__(self):
'''Debug dump string output of class'''
text = "%s:" % self.__class__.__name__
- text = text + "\n\tdn=%s" % self.site_dnstr
- text = text + "\n\toptions=0x%X" % self.site_options
+ text = text + "\n\tdn=%s" % self.site_dnstr
+ text = text + "\n\toptions=0x%X" % self.site_options
+ text = text + "\n\ttopo_generator=%s" % self.site_topo_generator
+ text = text + "\n\ttopo_failover=%d" % self.site_topo_failover
for key, dsa in self.dsa_table.items():
text = text + "\n%s" % dsa
return text
@@ -1101,7 +1706,7 @@ class Site(object):
class GraphNode(object):
"""A graph node describing a set of edges that should be directed to it.
-
+
Each edge is a connection for a particular naming context replica directed
from another node in the forest to this node.
"""
@@ -1127,7 +1732,7 @@ class GraphNode(object):
def add_edge_from(self, from_dsa_dnstr):
"""Add an edge from the dsa to our graph nodes edge from list
-
+
:param from_dsa_dnstr: the dsa that the edge emanates from
"""
assert from_dsa_dnstr is not None
@@ -1146,10 +1751,10 @@ class GraphNode(object):
def add_edges_from_connections(self, dsa):
"""For each nTDSConnection object associated with a particular
- DSA, we test if it implies an edge to this graph node (i.e.
- the "fromServer" attribute). If it does then we add an
- edge from the server unless we are over the max edges for this
- graph node
+ DSA, we test if it implies an edge to this graph node (i.e.
+ the "fromServer" attribute). If it does then we add an
+ edge from the server unless we are over the max edges for this
+ graph node
:param dsa: dsa with a dnstr equivalent to his graph node
"""
@@ -1186,41 +1791,13 @@ class GraphNode(object):
return
# Generate a new dnstr for this nTDSConnection
- dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
-
- connect = NTDSConnection(dnstr)
- 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
+ opt = dsdb.NTDSCONN_OPT_IS_GENERATED
+ flags = dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME + \
+ dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE
- header = drsblobs.scheduleHeader()
- header.type = 0
- header.offset = 20
-
- connect.schedule.headerArray = [ header ]
-
- # 168 byte instances of the 0x01 value. The low order 4 bits
- # of the byte equate to 15 minute intervals within a single hour.
- # There are 168 bytes because there are 168 hours in a full week
- # Effectively we are saying to perform replication at the end of
- # each hour of the week
- data = drsblobs.scheduleSlots()
- data.slots = [ 0x01 ] * 168
-
- connect.schedule.dataArray = [ data ]
+ dsa.create_connection(opt, flags, None, edge_dnstr, None)
+ return
- dsa.add_connection(dnstr, connect);
def has_sufficient_edges(self):
'''Return True if we have met the maximum "from edges" criteria'''
@@ -1229,6 +1806,7 @@ class GraphNode(object):
return False
+
class Transport(object):
"""Class defines a Inter-site transport found under Sites
"""
@@ -1237,7 +1815,9 @@ class Transport(object):
self.dnstr = dnstr
self.options = 0
self.guid = None
+ self.name = None
self.address_attr = None
+ self.bridgehead_list = []
def __str__(self):
'''Debug dump string output of Transport object'''
@@ -1246,16 +1826,21 @@ class Transport(object):
text = text + "\n\tguid=%s" % str(self.guid)
text = text + "\n\toptions=%d" % self.options
text = text + "\n\taddress_attr=%s" % self.address_attr
+ text = text + "\n\tname=%s" % self.name
+ for dnstr in self.bridgehead_list:
+ text = text + "\n\tbridgehead_list=%s" % dnstr
return text
def load_transport(self, samdb):
"""Given a Transport object with an prior initialization
- for the object's DN, search for the DN and load attributes
- from the samdb.
+ for the object's DN, search for the DN and load attributes
+ from the samdb.
"""
attrs = [ "objectGUID",
"options",
+ "name",
+ "bridgeheadServerListBL",
"transportAddressAttribute" ]
try:
res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
@@ -1271,9 +1856,20 @@ class Transport(object):
if "options" in msg:
self.options = int(msg["options"][0])
+
if "transportAddressAttribute" in msg:
self.address_attr = str(msg["transportAddressAttribute"][0])
+ if "name" in msg:
+ self.name = str(msg["name"][0])
+
+ if "bridgeheadServerListBL" in msg:
+ for value in msg["bridgeheadServerListBL"]:
+ dsdn = dsdb_Dn(samdb, value)
+ dnstr = str(dsdn.dn)
+ if dnstr not in self.bridgehead_list:
+ self.bridgehead_list.append(dnstr)
+ return
class RepsFromTo(object):
"""Class encapsulation of the NDR repsFromToBlob.
@@ -1367,6 +1963,12 @@ class RepsFromTo(object):
'source_dsa_obj_guid', 'source_dsa_invocation_id',
'consecutive_sync_failures', 'last_success',
'last_attempt' ]:
+
+ if item in ['replica_flags']:
+ self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
+ elif item in ['schedule']:
+ self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
+
setattr(self.__dict__['ndr_blob'].ctr, item, value)
elif item in ['dns_name1']:
@@ -1388,21 +1990,23 @@ class RepsFromTo(object):
self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
self.__dict__['dns_name2']
+ elif item in ['nc_dnstr']:
+ self.__dict__['nc_dnstr'] = value
+
+ elif item in ['to_be_deleted']:
+ self.__dict__['to_be_deleted'] = value
+
elif item in ['version']:
raise AttributeError, "Attempt to set readonly attribute %s" % item
else:
raise AttributeError, "Unknown attribute %s" % item
- 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
+ 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',
@@ -1426,7 +2030,171 @@ class RepsFromTo(object):
else:
return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
+ elif item in ['to_be_deleted']:
+ return self.__dict__['to_be_deleted']
+
+ elif item in ['nc_dnstr']:
+ return self.__dict__['nc_dnstr']
+
+ elif item in ['update_flags']:
+ return self.__dict__['update_flags']
+
raise AttributeError, "Unknwown attribute %s" % item
def is_modified(self):
return (self.update_flags != 0x0)
+
+ def set_unmodified(self):
+ self.__dict__['update_flags'] = 0x0
+
+class SiteLink(object):
+ """Class defines a site link found under sites
+ """
+
+ def __init__(self, dnstr):
+ self.dnstr = dnstr
+ self.options = 0
+ self.system_flags = 0
+ self.cost = 0
+ self.schedule = None
+ self.interval = None
+ self.site_list = []
+
+ def __str__(self):
+ '''Debug dump string output of Transport object'''
+
+ text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
+ text = text + "\n\toptions=%d" % self.options
+ text = text + "\n\tsystem_flags=%d" % self.system_flags
+ text = text + "\n\tcost=%d" % self.cost
+ text = text + "\n\tinterval=%s" % self.interval
+
+ if self.schedule is not None:
+ text = text + "\n\tschedule.size=%s" % self.schedule.size
+ text = text + "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
+ text = text + "\n\tschedule.numberOfSchedules=%s" % \
+ self.schedule.numberOfSchedules
+
+ for i, header in enumerate(self.schedule.headerArray):
+ text = text + "\n\tschedule.headerArray[%d].type=%d" % \
+ (i, header.type)
+ text = text + "\n\tschedule.headerArray[%d].offset=%d" % \
+ (i, header.offset)
+ text = text + "\n\tschedule.dataArray[%d].slots[ " % i
+ for slot in self.schedule.dataArray[i].slots:
+ text = text + "0x%X " % slot
+ text = text + "]"
+
+ for dnstr in self.site_list:
+ text = text + "\n\tsite_list=%s" % dnstr
+ return text
+
+ def load_sitelink(self, samdb):
+ """Given a siteLink object with an prior initialization
+ for the object's DN, search for the DN and load attributes
+ from the samdb.
+ """
+ attrs = [ "options",
+ "systemFlags",
+ "cost",
+ "schedule",
+ "replInterval",
+ "siteList" ]
+ try:
+ res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
+ attrs=attrs)
+
+ except ldb.LdbError, (enum, estr):
+ raise Exception("Unable to find SiteLink for (%s) - (%s)" %
+ (self.dnstr, estr))
+
+ msg = res[0]
+
+ if "options" in msg:
+ self.options = int(msg["options"][0])
+
+ if "systemFlags" in msg:
+ self.system_flags = int(msg["systemFlags"][0])
+
+ if "cost" in msg:
+ self.cost = int(msg["cost"][0])
+
+ if "replInterval" in msg:
+ self.interval = int(msg["replInterval"][0])
+
+ if "siteList" in msg:
+ for value in msg["siteList"]:
+ dsdn = dsdb_Dn(samdb, value)
+ dnstr = str(dsdn.dn)
+ if dnstr not in self.site_list:
+ self.site_list.append(dnstr)
+ return
+
+ def is_sitelink(self, site1_dnstr, site2_dnstr):
+ """Given a siteLink object, determine if it is a link
+ between the two input site DNs
+ """
+ if site1_dnstr in self.site_list and \
+ site2_dnstr in self.site_list:
+ return True
+ return False
+
+class VertexColor():
+ (unknown, white, black, red) = range(0, 4)
+
+class Vertex(object):
+ """Class encapsulation of a Site Vertex in the
+ intersite topology replication algorithm
+ """
+ def __init__(self, site, part):
+ self.site = site
+ self.part = part
+ self.color = VertexColor.unknown
+ return
+
+ def color_vertex(self):
+ """Color each vertex to indicate which kind of NC
+ replica it contains
+ """
+ # IF s contains one or more DCs with full replicas of the
+ # NC cr!nCName
+ # SET v.Color to COLOR.RED
+ # ELSEIF s contains one or more partial replicas of the NC
+ # SET v.Color to COLOR.BLACK
+ #ELSE
+ # SET v.Color to COLOR.WHITE
+
+ # set to minimum (no replica)
+ self.color = VertexColor.white
+
+ for dnstr, dsa in self.site.dsa_table.items():
+ rep = dsa.get_current_replica(self.part.nc_dnstr)
+ if rep is None:
+ continue
+
+ # We have a full replica which is the largest
+ # value so exit
+ if rep.is_partial() == False:
+ self.color = VertexColor.red
+ break
+ else:
+ self.color = VertexColor.black
+ return
+
+ def is_red(self):
+ assert(self.color != VertexColor.unknown)
+ return (self.color == VertexColor.red)
+
+ def is_black(self):
+ assert(self.color != VertexColor.unknown)
+ return (self.color == VertexColor.black)
+
+ def is_white(self):
+ assert(self.color != VertexColor.unknown)
+ return (self.color == VertexColor.white)
+
+##################################################
+# Global Functions
+##################################################
+def sort_dsa_by_guid(dsa1, dsa2):
+ return cmp(dsa1.dsa_guid, dsa2.dsa_guid)