summaryrefslogtreecommitdiff
path: root/source4/scripting/python
diff options
context:
space:
mode:
authorDave Craft <wimberosa@gmail.com>2011-12-04 11:08:56 -0600
committerAndrew Tridgell <tridge@samba.org>2011-12-08 11:48:17 +1100
commit819f11285d12041f2a22a6c92ebabb8a559886c5 (patch)
treec38b2a78c2615b3c7ae56b3414d3557314bb57c8 /source4/scripting/python
parent0a4746a20085a21bd8f28faf13bc5168f3ad5afb (diff)
downloadsamba-819f11285d12041f2a22a6c92ebabb8a559886c5.tar.gz
samba-819f11285d12041f2a22a6c92ebabb8a559886c5.tar.bz2
samba-819f11285d12041f2a22a6c92ebabb8a559886c5.zip
samba_kcc NTDSConnection translation
This is an advancement of samba_kcc to compute and commit the modification of a repsFrom on an NC Replica. The repsFrom is computed according to the MS tech spec for implied replicas of NTDSConnections. Proper maintenance of (DRS options, schedules, etc) from a NTDSConnection are now all present. New classes for inter-site transports, sites, and repsFrom) are now present in kcc_utils.py. Substantively this gets intra-site topology generation functional by committing the repsFrom that were computed from the DSA graph implemented in prior drops of samba_kcc Signed-off-by: Andrew Tridgell <tridge@samba.org>
Diffstat (limited to 'source4/scripting/python')
-rw-r--r--source4/scripting/python/samba/kcc_utils.py790
1 files changed, 682 insertions, 108 deletions
diff --git a/source4/scripting/python/samba/kcc_utils.py b/source4/scripting/python/samba/kcc_utils.py
index ac7449acd0..13bc2412d6 100644
--- a/source4/scripting/python/samba/kcc_utils.py
+++ b/source4/scripting/python/samba/kcc_utils.py
@@ -22,7 +22,11 @@ import uuid
from samba import dsdb
from samba.dcerpc import misc
+from samba.dcerpc import drsblobs
+from samba.dcerpc import drsuapi
from samba.common import dsdb_Dn
+from samba.ndr import ndr_unpack
+from samba.ndr import ndr_pack
class NCType:
(unknown, schema, domain, config, application) = range(0, 5)
@@ -36,20 +40,24 @@ class NamingContext:
def __init__(self, nc_dnstr, nc_guid=None, nc_sid=None):
"""Instantiate a NamingContext
:param nc_dnstr: NC dn string
- :param nc_guid: NC guid string
+ :param nc_guid: NC guid
:param nc_sid: NC sid
"""
- self.nc_dnstr = nc_dnstr
- self.nc_guid = nc_guid
- self.nc_sid = nc_sid
- self.nc_type = NCType.unknown
+ self.nc_dnstr = nc_dnstr
+ self.nc_guid = nc_guid
+ self.nc_sid = nc_sid
+ self.nc_type = NCType.unknown
return
def __str__(self):
'''Debug dump string output of class'''
- return "%s:\n\tdn=%s\n\tguid=%s\n\ttype=%s" % \
- (self.__class__.__name__, self.nc_dnstr,
- self.nc_guid, self.nc_type)
+ text = "%s:" % self.__class__.__name__
+ text = text + "\n\tnc_dnstr=%s" % self.nc_dnstr
+ text = text + "\n\tnc_guid=%s" % str(self.nc_guid)
+ text = text + "\n\tnc_sid=%s" % self.nc_sid
+ text = text + "\n\tnc_type=%s" % self.nc_type
+
+ return text
def is_schema(self):
'''Return True if NC is schema'''
@@ -79,7 +87,7 @@ class NamingContext:
self.nc_type = NCType.schema
elif self.nc_dnstr == str(samdb.get_config_basedn()):
self.nc_type = NCType.config
- elif self.nc_sid != None:
+ elif self.nc_sid is not None:
self.nc_type = NCType.domain
else:
self.nc_type = NCType.application
@@ -118,7 +126,6 @@ class NamingContext:
return
-
class NCReplica(NamingContext):
"""Class defines a naming context replica that is relative
to a specific DSA. This is a more specific form of
@@ -131,15 +138,18 @@ class NCReplica(NamingContext):
"""Instantiate a Naming Context Replica
:param dsa_guid: GUID of DSA where replica appears
:param nc_dnstr: NC dn string
- :param nc_guid: NC guid string
+ :param nc_guid: NC guid
:param nc_sid: NC sid
"""
- self.rep_dsa_dnstr = dsa_dnstr
- self.rep_dsa_guid = dsa_guid # GUID of DSA where this appears
- self.rep_default = False # replica for DSA's default domain
- self.rep_partial = False
- self.rep_ro = False
- self.rep_flags = 0
+ self.rep_dsa_dnstr = dsa_dnstr
+ self.rep_dsa_guid = dsa_guid
+ self.rep_default = False # replica for DSA's default domain
+ self.rep_partial = False
+ self.rep_ro = False
+ self.rep_instantiated_flags = 0
+
+ # RepsFromTo tuples
+ self.rep_repsFrom = []
# The (is present) test is a combination of being
# enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or
@@ -154,19 +164,25 @@ class NCReplica(NamingContext):
def __str__(self):
'''Debug dump string output of class'''
- text = "default=%s" % self.rep_default + \
- ":ro=%s" % self.rep_ro + \
- ":partial=%s" % self.rep_partial + \
- ":present=%s" % self.is_present()
- return "%s\n\tdsaguid=%s\n\t%s" % \
- (NamingContext.__str__(self), self.rep_dsa_guid, text)
-
- def set_replica_flags(self, flags=None):
- '''Set or clear NC replica flags'''
+ text = "%s:" % self.__class__.__name__
+ text = text + "\n\tdsa_dnstr=%s" % self.rep_dsa_dnstr
+ text = text + "\n\tdsa_guid=%s" % str(self.rep_dsa_guid)
+ text = text + "\n\tdefault=%s" % self.rep_default
+ text = text + "\n\tro=%s" % self.rep_ro
+ text = text + "\n\tpartial=%s" % self.rep_partial
+ text = text + "\n\tpresent=%s" % self.is_present()
+
+ for rep in self.rep_repsFrom:
+ text = text + "\n%s" % rep
+
+ return "%s\n%s" % (NamingContext.__str__(self), text)
+
+ def set_instantiated_flags(self, flags=None):
+ '''Set or clear NC replica instantiated flags'''
if (flags == None):
- self.rep_flags = 0
+ self.rep_instantiated_flags = 0
else:
- self.rep_flags = flags
+ self.rep_instantiated_flags = flags
return
def identify_by_dsa_attr(self, samdb, attr):
@@ -239,10 +255,90 @@ class NCReplica(NamingContext):
set then the NC replica is not present (false)
"""
if self.rep_present_criteria_one and \
- self.rep_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0:
+ self.rep_instantiated_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0:
return True
return False
+ def load_repsFrom(self, samdb):
+ """Given an NC replica which has been discovered thru the nTDSDSA
+ database object, load the repsFrom attribute for the local replica.
+ held by my dsa. The repsFrom attribute is not replicated so this
+ attribute is relative only to the local DSA that the samdb exists on
+ """
+ try:
+ res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
+ attrs=[ "repsFrom" ])
+
+ except ldb.LdbError, (enum, estr):
+ raise Exception("Unable to find NC for (%s) - (%s)" % \
+ (self.nc_dnstr, estr))
+ return
+
+ msg = res[0]
+
+ # Possibly no repsFrom if this is a singleton DC
+ if "repsFrom" in msg:
+ for value in msg["repsFrom"]:
+ rep = RepsFromTo(self.nc_dnstr, \
+ ndr_unpack(drsblobs.repsFromToBlob, value))
+ self.rep_repsFrom.append(rep)
+ return
+
+ def commit_repsFrom(self, samdb):
+ """Commit repsFrom to the database"""
+
+ # XXX - This is not truly correct according to the MS-TECH
+ # docs. To commit a repsFrom we should be using RPCs
+ # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
+ # IDL_DRSReplicaDel to affect a repsFrom change.
+ #
+ # Those RPCs are missing in samba, so I'll have to
+ # implement them to get this to more accurately
+ # reflect the reference docs. As of right now this
+ # commit to the database will work as its what the
+ # older KCC also did
+ modify = False
+ newreps = []
+
+ for repsFrom in self.rep_repsFrom:
+
+ # Leave out any to be deleted from
+ # replacement list
+ if repsFrom.to_be_deleted == True:
+ modify = True
+ continue
+
+ if repsFrom.is_modified():
+ modify = True
+
+ newreps.append(ndr_pack(repsFrom.ndr_blob))
+
+ # Nothing to do if no reps have been modified or
+ # need to be deleted. Leave database record "as is"
+ if modify == False:
+ return
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, self.nc_dnstr)
+
+ m["repsFrom"] = \
+ ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsFrom")
+
+ try:
+ samdb.modify(m)
+
+ except ldb.LdbError, estr:
+ raise Exception("Could not set repsFrom for (%s) - (%s)" % \
+ (self.dsa_dnstr, estr))
+ return
+
+ def load_fsmo_roles(self, samdb):
+ # XXX - to be implemented
+ return
+
+ def is_fsmo_role_owner(self, dsa_dnstr):
+ # XXX - to be implemented
+ return False
class DirectoryServiceAgent:
@@ -255,33 +351,50 @@ class DirectoryServiceAgent:
self.dsa_guid = None
self.dsa_ivid = None
self.dsa_is_ro = False
- self.dsa_is_gc = False
+ self.dsa_options = 0
self.dsa_behavior = 0
self.default_dnstr = None # default domain dn string for dsa
- # NCReplicas for this dsa.
+ # NCReplicas for this dsa that are "present"
# Indexed by DN string of naming context
- self.rep_table = {}
+ self.current_rep_table = {}
- # NTDSConnections for this dsa.
- # Indexed by DN string of connection
+ # NCReplicas for this dsa that "should be present"
+ # Indexed by DN string of naming context
+ self.needed_rep_table = {}
+
+ # NTDSConnections for this dsa. These are current
+ # valid connections that are committed or "to be committed"
+ # in the database. Indexed by DN string of connection
self.connect_table = {}
+
return
def __str__(self):
'''Debug dump string output of class'''
- text = ""
- if self.dsa_dnstr:
- text = text + "\n\tdn=%s" % self.dsa_dnstr
- if self.dsa_guid:
- text = text + "\n\tguid=%s" % str(self.dsa_guid)
- if self.dsa_ivid:
- text = text + "\n\tivid=%s" % str(self.dsa_ivid)
-
- text = text + "\n\tro=%s:gc=%s" % (self.dsa_is_ro, self.dsa_is_gc)
- return "%s:%s\n%s\n%s" % (self.__class__.__name__, text,
- self.dumpstr_replica_table(),
- self.dumpstr_connect_table())
+
+ text = "%s:" % self.__class__.__name__
+ if self.dsa_dnstr is not None:
+ text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
+ if self.dsa_guid is not None:
+ text = text + "\n\tdsa_guid=%s" % str(self.dsa_guid)
+ if self.dsa_ivid is not None:
+ text = text + "\n\tdsa_ivid=%s" % str(self.dsa_ivid)
+
+ text = text + "\n\tro=%s" % self.is_ro()
+ text = text + "\n\tgc=%s" % self.is_gc()
+
+ text = text + "\ncurrent_replica_table:"
+ text = text + "\n%s" % self.dumpstr_current_replica_table()
+ text = text + "\nneeded_replica_table:"
+ text = text + "\n%s" % self.dumpstr_needed_replica_table()
+ text = text + "\nconnect_table:"
+ text = text + "\n%s" % self.dumpstr_connect_table()
+
+ return text
+
+ def get_current_replica(self, nc_dnstr):
+ return self.current_rep_table[nc_dnstr]
def is_ro(self):
'''Returns True if dsa a read only domain controller'''
@@ -289,7 +402,9 @@ class DirectoryServiceAgent:
def is_gc(self):
'''Returns True if dsa hosts a global catalog'''
- return self.dsa_is_gc
+ if (self.options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
+ return True
+ return False
def is_minimum_behavior(self, version):
"""Is dsa at minimum windows level greater than or
@@ -301,6 +416,27 @@ class DirectoryServiceAgent:
return True
return False
+ def should_translate_ntdsconn(self):
+ """Returns True if DSA object allows NTDSConnection
+ translation in its options. False otherwise.
+ """
+ if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0:
+ return False
+ return True
+
+ def get_rep_tables(self):
+ """Return DSA current and needed replica tables
+ """
+ return self.current_rep_table, self.needed_rep_table
+
+ def get_parent_dnstr(self):
+ """Drop the leading portion of the DN string
+ (e.g. CN=NTDS Settings,) which will give us
+ the parent DN string of this object
+ """
+ head, sep, tail = self.dsa_dnstr.partition(',')
+ return tail
+
def load_dsa(self, samdb):
"""Method to load a DSA from the samdb. Prior initialization
has given us the DN of the DSA that we are to load. This
@@ -324,7 +460,7 @@ class DirectoryServiceAgent:
return
msg = res[0]
- self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID",
+ self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID", \
msg["objectGUID"][0]))
# RODCs don't originate changes and thus have no invocationId,
@@ -333,11 +469,8 @@ class DirectoryServiceAgent:
self.dsa_ivid = misc.GUID(samdb.schema_format_value("objectGUID",
msg["invocationId"][0]))
- if "options" in msg and \
- ((int(msg["options"][0]) & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0):
- self.dsa_is_gc = True
- else:
- self.dsa_is_gc = False
+ if "options" in msg:
+ self.options = int(msg["options"][0])
if "msDS-isRODC" in msg and msg["msDS-isRODC"][0] == "TRUE":
self.dsa_is_ro = True
@@ -348,7 +481,7 @@ class DirectoryServiceAgent:
self.dsa_behavior = int(msg['msDS-Behavior-Version'][0])
# Load the NC replicas that are enumerated on this dsa
- self.load_replica_table(samdb)
+ self.load_current_replica_table(samdb)
# Load the nTDSConnection that are enumerated on this dsa
self.load_connection_table(samdb)
@@ -356,7 +489,7 @@ class DirectoryServiceAgent:
return
- def load_replica_table(self, samdb):
+ def load_current_replica_table(self, samdb):
"""Method to load the NC replica's listed for DSA object. This
method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs,
hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs,
@@ -373,7 +506,7 @@ class DirectoryServiceAgent:
# not RODC - default, config, schema, app NCs
"msDS-hasMasterNCs",
# domain NC partial replicas
- "hasPartialReplicANCs",
+ "hasPartialReplicaNCs",
# default domain NC
"msDS-HasDomainNCs",
# RODC only - default, config, schema, app NCs
@@ -423,17 +556,17 @@ class DirectoryServiceAgent:
raise Exception("Missing GUID for (%s) - (%s: %s)" % \
(self.dsa_dnstr, k, value))
else:
- guidstr = str(misc.GUID(guid))
+ guid = misc.GUID(guid)
if not dnstr in tmp_table:
rep = NCReplica(self.dsa_dnstr, self.dsa_guid,
- dnstr, guidstr, sid)
+ dnstr, guid, sid)
tmp_table[dnstr] = rep
else:
rep = tmp_table[dnstr]
if k == "msDS-HasInstantiatedNCs":
- rep.set_replica_flags(flags)
+ rep.set_instantiated_flags(flags)
continue
rep.identify_by_dsa_attr(samdb, k)
@@ -447,7 +580,16 @@ class DirectoryServiceAgent:
return
# Assign our newly built NC replica table to this dsa
- self.rep_table = tmp_table
+ self.current_rep_table = tmp_table
+ return
+
+ def add_needed_replica(self, rep):
+ """Method to add a NC replica that "should be present" to the
+ needed_rep_table if not already in the table
+ """
+ if not rep.nc_dnstr in self.needed_rep_table.keys():
+ self.needed_rep_table[rep.nc_dnstr] = rep
+
return
def load_connection_table(self, samdb):
@@ -480,14 +622,14 @@ class DirectoryServiceAgent:
def commit_connection_table(self, samdb):
"""Method to commit any uncommitted nTDSConnections
- that are in our table. These would be newly identified
- connections that are marked as (committed = False)
+ that are in our table. These would be identified
+ connections that are marked to be added or deleted
:param samdb: database to commit DSA connection list to
"""
for dnstr, connect in self.connect_table.items():
connect.commit_connection(samdb)
- def add_connection_by_dnstr(self, dnstr, connect):
+ def add_connection(self, dnstr, connect):
self.connect_table[dnstr] = connect
return
@@ -502,14 +644,24 @@ class DirectoryServiceAgent:
return connect
return None
- def dumpstr_replica_table(self):
- '''Debug dump string output of replica table'''
+ def dumpstr_current_replica_table(self):
+ '''Debug dump string output of current replica table'''
+ text=""
+ for k in self.current_rep_table.keys():
+ if text:
+ text = text + "\n%s" % self.current_rep_table[k]
+ else:
+ text = "%s" % self.current_rep_table[k]
+ return text
+
+ def dumpstr_needed_replica_table(self):
+ '''Debug dump string output of needed replica table'''
text=""
- for k in self.rep_table.keys():
+ for k in self.needed_rep_table.keys():
if text:
- text = text + "\n%s" % self.rep_table[k]
+ text = text + "\n%s" % self.needed_rep_table[k]
else:
- text = "%s" % self.rep_table[k]
+ text = "%s" % self.needed_rep_table[k]
return text
def dumpstr_connect_table(self):
@@ -526,23 +678,50 @@ class NTDSConnection():
"""Class defines a nTDSConnection found under a DSA
"""
def __init__(self, dnstr):
- self.dnstr = dnstr
- self.enabled = False
- self.committed = False # appears in database
- self.options = 0
- self.flags = 0
- self.from_dnstr = None
- self.schedulestr = None
+ self.dnstr = dnstr
+ self.enabled = False
+ self.committed = False # new connection needs to be committed
+ self.options = 0
+ self.flags = 0
+ self.transport_dnstr = None
+ self.transport_guid = None
+ self.from_dnstr = None
+ self.from_guid = None
+ self.schedule = None
return
def __str__(self):
'''Debug dump string output of NTDSConnection object'''
- text = "%s: %s" % (self.__class__.__name__, self.dnstr)
- text = text + "\n\tenabled: %s" % self.enabled
- text = text + "\n\tcommitted: %s" % self.committed
- text = text + "\n\toptions: 0x%08X" % self.options
- text = text + "\n\tflags: 0x%08X" % self.flags
- text = text + "\n\tfrom_dn: %s" % self.from_dnstr
+
+ text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
+ text = text + "\n\tenabled=%s" % self.enabled
+ text = text + "\n\tcommitted=%s" % self.committed
+ text = text + "\n\toptions=0x%08X" % self.options
+ text = text + "\n\tflags=0x%08X" % self.flags
+ text = text + "\n\ttransport_dn=%s" % self.transport_dnstr
+
+ if self.transport_guid is not None:
+ text = text + "\n\ttransport_guid=%s" % str(self.transport_guid)
+
+ text = text + "\n\tfrom_dn=%s" % self.from_dnstr
+ text = text + "\n\tfrom_guid=%s" % str(self.from_guid)
+
+ if self.schedule is not None:
+ text = text + "\n\tschedule.size=%s" % self.schedule.size
+ text = text + "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth
+ text = text + "\n\tschedule.numberOfSchedules=%s" % \
+ self.schedule.numberOfSchedules
+
+ for i, header in enumerate(self.schedule.headerArray):
+ text = text + "\n\tschedule.headerArray[%d].type=%d" % \
+ (i, header.type)
+ text = text + "\n\tschedule.headerArray[%d].offset=%d" % \
+ (i, header.offset)
+ text = text + "\n\tschedule.dataArray[%d].slots[ " % i
+ for slot in self.schedule.dataArray[i].slots:
+ text = text + "0x%X " % slot
+ text = text + "]"
+
return text
def load_connection(self, samdb):
@@ -551,14 +730,16 @@ class NTDSConnection():
from the samdb.
Raises an Exception on error.
"""
+ controls = ["extended_dn:1:1"]
attrs = [ "options",
"enabledConnection",
"schedule",
+ "transportType",
"fromServer",
"systemFlags" ]
try:
res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
- attrs=attrs)
+ attrs=attrs, controls=controls)
except ldb.LdbError, (enum, estr):
raise Exception("Unable to find nTDSConnection for (%s) - (%s)" % \
@@ -574,14 +755,30 @@ class NTDSConnection():
self.enabled = True
if "systemFlags" in msg:
self.flags = int(msg["systemFlags"][0])
+ if "transportType" in msg:
+ dsdn = dsdb_Dn(samdb, msg["tranportType"][0])
+ guid = dsdn.dn.get_extended_component('GUID')
+
+ assert guid is not None
+ self.transport_guid = misc.GUID(guid)
+
+ self.transport_dnstr = str(dsdn.dn)
+ assert self.transport_dnstr is not None
+
if "schedule" in msg:
- self.schedulestr = msg["schedule"][0]
+ self.schedule = ndr_unpack(drsblobs.replSchedule, msg["schedule"][0])
+
if "fromServer" in msg:
dsdn = dsdb_Dn(samdb, msg["fromServer"][0])
+ guid = dsdn.dn.get_extended_component('GUID')
+
+ assert guid is not None
+ self.from_guid = misc.GUID(guid)
+
self.from_dnstr = str(dsdn.dn)
- assert self.from_dnstr != None
+ assert self.from_dnstr is not None
- # Appears as committed in the database
+ # Was loaded from database so connection is currently committed
self.committed = True
return
@@ -589,12 +786,109 @@ class NTDSConnection():
"""Given a NTDSConnection object that is not committed in the
sam database, perform a commit action.
"""
- if self.committed: # nothing to do
+ # nothing to do
+ if self.committed == True:
return
- # XXX - not yet written
+ # First verify we don't have this entry to ensure nothing
+ # is programatically amiss
+ try:
+ msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
+ found = True
+
+ except ldb.LdbError, (enum, estr):
+ if enum == ldb.ERR_NO_SUCH_OBJECT:
+ found = False
+ else:
+ raise Exception("Unable to search for (%s) - (%s)" % \
+ (self.dnstr, estr))
+ if found:
+ raise Exception("nTDSConnection for (%s) already exists!" % self.dnstr)
+
+ if self.enabled:
+ enablestr = "TRUE"
+ else:
+ enablestr = "FALSE"
+
+ # Prepare a message for adding to the samdb
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, self.dnstr)
+
+ m["objectClass"] = \
+ ldb.MessageElement("nTDSConnection", ldb.FLAG_MOD_ADD, \
+ "objectClass")
+ m["showInAdvancedViewOnly"] = \
+ ldb.MessageElement("TRUE", ldb.FLAG_MOD_ADD, \
+ "showInAdvancedViewOnly")
+ m["enabledConnection"] = \
+ ldb.MessageElement(enablestr, ldb.FLAG_MOD_ADD, "enabledConnection")
+ m["fromServer"] = \
+ ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_ADD, "fromServer")
+ m["options"] = \
+ ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options")
+ m["systemFlags"] = \
+ ldb.MessageElement(str(self.flags), ldb.FLAG_MOD_ADD, "systemFlags")
+
+ if self.schedule is not None:
+ m["schedule"] = \
+ ldb.MessageElement(ndr_pack(self.schedule),
+ ldb.FLAG_MOD_ADD, "schedule")
+ try:
+ samdb.add(m)
+ except ldb.LdbError, (enum, estr):
+ raise Exception("Could not add nTDSConnection for (%s) - (%s)" % \
+ (self.dnstr, estr))
+ self.committed = True
return
+ def is_schedule_minimum_once_per_week(self):
+ """Returns True if our schedule includes at least one
+ replication interval within the week. False otherwise
+ """
+ if self.schedule is None or self.schedule.dataArray[0] is None:
+ return False
+
+ for slot in self.schedule.dataArray[0].slots:
+ if (slot & 0x0F) != 0x0:
+ return True
+ return False
+
+ def convert_schedule_to_repltimes(self):
+ """Convert NTDS Connection schedule to replTime schedule.
+ NTDS Connection schedule slots are double the size of
+ the replTime slots but the top portion of the NTDS
+ Connection schedule slot (4 most significant bits in
+ uchar) are unused. The 4 least significant bits have
+ the same (15 minute interval) bit positions as replTimes.
+ We thus pack two elements of the NTDS Connection schedule
+ slots into one element of the replTimes slot
+ If no schedule appears in NTDS Connection then a default
+ of 0x11 is set in each replTimes slot as per behaviour
+ noted in a Windows DC. That default would cause replication
+ within the last 15 minutes of each hour.
+ """
+ times = [0x11] * 84
+
+ for i, slot in enumerate(times):
+ if self.schedule is not None and \
+ self.schedule.dataArray[0] is not None:
+ slot = (self.schedule.dataArray[0].slots[i*2] & 0xF) << 4 | \
+ (self.schedule.dataArray[0].slots[i*2] & 0xF)
+ return times
+
+ def is_rodc_topology(self):
+ """Returns True if NTDS Connection specifies RODC
+ topology only
+ """
+ if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
+ return False
+ return True
+
+ def is_enabled(self):
+ """Returns True if NTDS Connection is enabled
+ """
+ return self.enabled
+
def get_from_dnstr(self):
'''Return fromServer dn string attribute'''
return self.from_dnstr
@@ -659,11 +953,11 @@ class Partition(NamingContext):
raise Exception("Missing GUID for (%s) - (%s: %s)" % \
(self.partstr, k, value))
else:
- guidstr = str(misc.GUID(guid))
+ guid = misc.GUID(guid)
if k == "nCName":
self.nc_dnstr = str(dsdn.dn)
- self.nc_guid = guidstr
+ self.nc_guid = guid
self.nc_sid = sid
continue
@@ -744,6 +1038,7 @@ class Site:
def __init__(self, site_dnstr):
self.site_dnstr = site_dnstr
self.site_options = 0
+ self.dsa_table = {}
return
def load_site(self, samdb):
@@ -762,13 +1057,53 @@ class Site:
msg = res[0]
if "options" in msg:
self.site_options = int(msg["options"][0])
+
+ self.load_all_dsa(samdb)
return
- def is_same_site(self, target_dsa):
- '''Determine if target dsa is in this site'''
- if self.site_dnstr in target_dsa.dsa_dnstr:
- return True
- return False
+ def load_all_dsa(self, samdb):
+ """Discover all nTDSDSA thru the sites entry and
+ instantiate and load the DSAs. Each dsa is inserted
+ into the dsa_table by dn string.
+ Raises an Exception on error.
+ """
+ try:
+ res = samdb.search(self.site_dnstr,
+ scope=ldb.SCOPE_SUBTREE,
+ expression="(objectClass=nTDSDSA)")
+ except ldb.LdbError, (enum, estr):
+ raise Exception("Unable to find nTDSDSAs - (%s)" % estr)
+
+ for msg in res:
+ dnstr = str(msg.dn)
+
+ # already loaded
+ if dnstr in self.dsa_table.keys():
+ continue
+
+ dsa = DirectoryServiceAgent(dnstr)
+
+ dsa.load_dsa(samdb)
+
+ # Assign this dsa to my dsa table
+ # and index by dsa dn
+ self.dsa_table[dnstr] = dsa
+ return
+
+ def get_dsa_by_guidstr(self, guidstr):
+ for dsa in self.dsa_table.values():
+ if str(dsa.dsa_guid) == guidstr:
+ return dsa
+ return None
+
+ def get_dsa(self, dnstr):
+ """Return a previously loaded DSA object by consulting
+ the sites dsa_table for the provided DSA dn string
+ Returns None if DSA doesn't exist
+ """
+ if dnstr in self.dsa_table.keys():
+ return self.dsa_table[dnstr]
+ return None
def is_intrasite_topology_disabled(self):
'''Returns True if intrasite topology is disabled for site'''
@@ -784,6 +1119,15 @@ class Site:
return True
return False
+ def __str__(self):
+ '''Debug dump string output of class'''
+ text = "%s:" % self.__class__.__name__
+ text = text + "\n\tdn=%s" % self.site_dnstr
+ text = text + "\n\toptions=0x%X" % self.site_options
+ for key, dsa in self.dsa_table.items():
+ text = text + "\n%s" % dsa
+ return text
+
class GraphNode:
"""This is a graph node describing a set of edges that should be
@@ -801,16 +1145,19 @@ class GraphNode:
self.edge_from = []
def __str__(self):
- text = "%s: %s" % (self.__class__.__name__, self.dsa_dnstr)
- for edge in self.edge_from:
- text = text + "\n\tedge from: %s" % edge
+ text = "%s:" % self.__class__.__name__
+ text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
+ text = text + "\n\tmax_edges=%d" % self.max_edges
+
+ for i, edge in enumerate(self.edge_from):
+ text = text + "\n\tedge_from[%d]=%s" % (i, edge)
return text
def add_edge_from(self, from_dsa_dnstr):
"""Add an edge from the dsa to our graph nodes edge from list
:param from_dsa_dnstr: the dsa that the edge emanates from
"""
- assert from_dsa_dnstr != None
+ assert from_dsa_dnstr is not None
# No edges from myself to myself
if from_dsa_dnstr == self.dsa_dnstr:
@@ -855,8 +1202,7 @@ class GraphNode:
# the DC on which ri "is present".
#
# c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
- if connect and \
- connect.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
+ if connect and connect.is_rodc_topology() == False:
exists = True
else:
exists = False
@@ -870,16 +1216,38 @@ class GraphNode:
dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
connect = NTDSConnection(dnstr)
- connect.enabled = True
- connect.committed = False
- connect.from_dnstr = edge_dnstr
- connect.options = dsdb.NTDSCONN_OPT_IS_GENERATED
- connect.flags = dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME + \
- dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE
+ connect.committed = False
+ connect.enabled = True
+ connect.from_dnstr = edge_dnstr
+ connect.options = dsdb.NTDSCONN_OPT_IS_GENERATED
+ connect.flags = dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME + \
+ dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE
+
+ # Create schedule. Attribute valuse set according to MS-TECH
+ # intrasite connection creation document
+ connect.schedule = drsblobs.schedule()
+
+ connect.schedule.size = 188
+ connect.schedule.bandwidth = 0
+ connect.schedule.numberOfSchedules = 1
- # XXX I need to write the schedule blob
+ header = drsblobs.scheduleHeader()
+ header.type = 0
+ header.offset = 20
- dsa.add_connection_by_dnstr(dnstr, connect);
+ connect.schedule.headerArray = [ header ]
+
+ # 168 byte instances of the 0x01 value. The low order 4 bits
+ # of the byte equate to 15 minute intervals within a single hour.
+ # There are 168 bytes because there are 168 hours in a full week
+ # Effectively we are saying to perform replication at the end of
+ # each hour of the week
+ data = drsblobs.scheduleSlots()
+ data.slots = [ 0x01 ] * 168
+
+ connect.schedule.dataArray = [ data ]
+
+ dsa.add_connection(dnstr, connect);
return
@@ -888,3 +1256,209 @@ class GraphNode:
if len(self.edge_from) >= self.max_edges:
return True
return False
+
+class Transport():
+ """Class defines a Inter-site transport found under Sites
+ """
+ def __init__(self, dnstr):
+ self.dnstr = dnstr
+ self.options = 0
+ self.guid = None
+ self.address_attr = None
+ return
+
+ def __str__(self):
+ '''Debug dump string output of Transport object'''
+
+ text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr)
+ text = text + "\n\tguid=%s" % str(self.guid)
+ text = text + "\n\toptions=%d" % self.options
+ text = text + "\n\taddress_attr=%s" % self.address_attr
+
+ return text
+
+ def load_transport(self, samdb):
+ """Given a Transport object with an prior initialization
+ for the object's DN, search for the DN and load attributes
+ from the samdb.
+ Raises an Exception on error.
+ """
+ attrs = [ "objectGUID",
+ "options",
+ "transportAddressAttribute" ]
+ try:
+ res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
+ attrs=attrs)
+
+ except ldb.LdbError, (enum, estr):
+ raise Exception("Unable to find Transport for (%s) - (%s)" % \
+ (self.dnstr, estr))
+ return
+
+ msg = res[0]
+ self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
+ msg["objectGUID"][0]))
+
+ if "options" in msg:
+ self.options = int(msg["options"][0])
+ if "transportAddressAttribute" in msg:
+ self.address_attr = str(msg["transportAddressAttribute"][0])
+
+ return
+
+class RepsFromTo:
+ """Class encapsulation of the NDR repsFromToBlob.
+ Removes the necessity of external code having to
+ understand about other_info or manipulation of
+ update flags.
+ """
+ def __init__(self, nc_dnstr=None, ndr_blob=None):
+
+ self.__dict__['to_be_deleted'] = False
+ self.__dict__['nc_dnstr'] = nc_dnstr
+ self.__dict__['update_flags'] = 0x0
+
+ # WARNING:
+ #
+ # There is a very subtle bug here with python
+ # and our NDR code. If you assign directly to
+ # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
+ # then a proper python GC reference count is not
+ # maintained.
+ #
+ # To work around this we maintain an internal
+ # reference to "dns_name(x)" and "other_info" elements
+ # of repsFromToBlob. This internal reference
+ # is hidden within this class but it is why you
+ # see statements like this below:
+ #
+ # self.__dict__['ndr_blob'].ctr.other_info = \
+ # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
+ #
+ # That would appear to be a redundant assignment but
+ # it is necessary to hold a proper python GC reference
+ # count.
+ if ndr_blob is None:
+ self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
+ self.__dict__['ndr_blob'].version = 0x1
+ self.__dict__['dns_name1'] = None
+ self.__dict__['dns_name2'] = None
+
+ self.__dict__['ndr_blob'].ctr.other_info = \
+ self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
+
+ else:
+ self.__dict__['ndr_blob'] = ndr_blob
+ self.__dict__['other_info'] = ndr_blob.ctr.other_info
+
+ if ndr_blob.version == 0x1:
+ self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
+ self.__dict__['dns_name2'] = None
+ else:
+ self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
+ self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
+ return
+
+ def __str__(self):
+ '''Debug dump string output of class'''
+
+ text = "%s:" % self.__class__.__name__
+ text = text + "\n\tdnstr=%s" % self.nc_dnstr
+ text = text + "\n\tupdate_flags=0x%X" % self.update_flags
+
+ text = text + "\n\tversion=%d" % self.version
+ text = text + "\n\tsource_dsa_obj_guid=%s" % \
+ str(self.source_dsa_obj_guid)
+ text = text + "\n\tsource_dsa_invocation_id=%s" % \
+ str(self.source_dsa_invocation_id)
+ text = text + "\n\ttransport_guid=%s" % \
+ str(self.transport_guid)
+ text = text + "\n\treplica_flags=0x%X" % \
+ self.replica_flags
+ text = text + "\n\tconsecutive_sync_failures=%d" % \
+ self.consecutive_sync_failures
+ text = text + "\n\tlast_success=%s" % \
+ self.last_success
+ text = text + "\n\tlast_attempt=%s" % \
+ self.last_attempt
+ text = text + "\n\tdns_name1=%s" % \
+ str(self.dns_name1)
+ text = text + "\n\tdns_name2=%s" % \
+ str(self.dns_name2)
+ text = text + "\n\tschedule[ "
+ for slot in self.schedule:
+ text = text + "0x%X " % slot
+ text = text + "]"
+
+ return text
+
+ def __setattr__(self, item, value):
+
+ if item in [ 'schedule', 'replica_flags', 'transport_guid', \
+ 'source_dsa_obj_guid', 'source_dsa_invocation_id', \
+ 'consecutive_sync_failures', 'last_success', \
+ 'last_attempt' ]:
+ setattr(self.__dict__['ndr_blob'].ctr, item, value)
+
+ elif item in ['dns_name1']:
+ self.__dict__['dns_name1'] = value
+
+ if self.__dict__['ndr_blob'].version == 0x1:
+ self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
+ self.__dict__['dns_name1']
+ else:
+ self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
+ self.__dict__['dns_name1']
+
+ elif item in ['dns_name2']:
+ self.__dict__['dns_name2'] = value
+
+ if self.__dict__['ndr_blob'].version == 0x1:
+ raise AttributeError(item)
+ else:
+ self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
+ self.__dict__['dns_name2']
+
+ elif item in ['version']:
+ raise AttributeError, "Attempt to set readonly attribute %s" % item
+ else:
+ raise AttributeError, "Unknown attribute %s" % item
+
+ if item in ['replica_flags']:
+ self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
+ elif item in ['schedule']:
+ self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
+ else:
+ self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
+
+ return
+
+ def __getattr__(self, item):
+ """Overload of RepsFromTo attribute retrieval. Allows
+ external code to ignore substructures within the blob
+ """
+ if item in [ 'schedule', 'replica_flags', 'transport_guid', \
+ 'source_dsa_obj_guid', 'source_dsa_invocation_id', \
+ 'consecutive_sync_failures', 'last_success', \
+ 'last_attempt' ]:
+ return getattr(self.__dict__['ndr_blob'].ctr, item)
+
+ elif item in ['version']:
+ return self.__dict__['ndr_blob'].version
+
+ elif item in ['dns_name1']:
+ if self.__dict__['ndr_blob'].version == 0x1:
+ return self.__dict__['ndr_blob'].ctr.other_info.dns_name
+ else:
+ return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
+
+ elif item in ['dns_name2']:
+ if self.__dict__['ndr_blob'].version == 0x1:
+ raise AttributeError(item)
+ else:
+ return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
+
+ raise AttributeError, "Unknwown attribute %s" % item
+
+ def is_modified(self):
+ return (self.update_flags != 0x0)