From ab1f896c5152dfd10609ac146eaaecd1bd2d5b70 Mon Sep 17 00:00:00 2001 From: Dave Craft Date: Wed, 11 Jan 2012 08:11:35 -0600 Subject: KCC importldif/exportldif and intersite topology Add options for extracting an LDIF file from a database and reimporting the LDIF into a schema-less database for subsequent topology test/debug. Add intersite topology generation with computation of ISTG and bridgehead servers Signed-off-by: Andrew Tridgell Autobuild-User: Andrew Tridgell Autobuild-Date: Sat Jan 14 07:45:11 CET 2012 on sn-devel-104 --- source4/scripting/bin/samba_kcc | 1629 ++++++++++++++++++++++++--- source4/scripting/python/samba/kcc_utils.py | 1176 +++++++++++++++---- 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 . 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="") + +parser.add_option("--importldif", \ + help="import topology ldif file", \ + type=str, metavar="") + +parser.add_option("--exportldif", \ + help="export topology ldif file", \ + type=str, metavar="") + +parser.add_option("-H", "--URL" , \ + help="LDB URL for database or target server", \ + type=str, metavar="", dest="dburl") + +parser.add_option("--tmpdb", \ + help="schemaless database file to create for ldif import", \ + type=str, metavar="") 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=" + else: + text = text + "\n\tnc_sid=" + 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) -- cgit