diff options
author | Dave Craft <wimberosa@gmail.com> | 2011-11-03 12:39:53 -0500 |
---|---|---|
committer | Andrew Tridgell <tridge@samba.org> | 2011-11-07 10:02:45 +1100 |
commit | d85b6edf8cdfd953ce96a50466595384db4c698b (patch) | |
tree | 9223d3a0a6a9b7bdc3b2bdfc5a1bc774a1c28089 /source4/scripting | |
parent | c78dac4fde0fdcdfe44a4bc98da30f43cf32ff6c (diff) | |
download | samba-d85b6edf8cdfd953ce96a50466595384db4c698b.tar.gz samba-d85b6edf8cdfd953ce96a50466595384db4c698b.tar.bz2 samba-d85b6edf8cdfd953ce96a50466595384db4c698b.zip |
samba_kcc addtion
Scaffolding and initial implementations of
portions of the KCC in python. This code currently
properly computes the graph nodes for the intrasite
topology as well as enumerating all steps for a full
run of the KCC.
Signed-off-by: Andrew Tridgell <tridge@samba.org>
Diffstat (limited to 'source4/scripting')
-rwxr-xr-x | source4/scripting/bin/samba_kcc | 703 | ||||
-rw-r--r-- | source4/scripting/bin/wscript_build | 1 | ||||
-rw-r--r-- | source4/scripting/wscript_build | 2 |
3 files changed, 705 insertions, 1 deletions
diff --git a/source4/scripting/bin/samba_kcc b/source4/scripting/bin/samba_kcc new file mode 100755 index 0000000000..10c51d3f00 --- /dev/null +++ b/source4/scripting/bin/samba_kcc @@ -0,0 +1,703 @@ +#!/usr/bin/env python +# +# Compute our KCC topology +# +# Copyright (C) Dave Craft 2011 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import random + +# ensure we get messages out immediately, so they get in the samba logs, +# and don't get swallowed by a timeout +os.environ['PYTHONUNBUFFERED'] = '1' + +# forcing GMT avoids a problem in some timezones with kerberos. Both MIT +# heimdal can get mutual authentication errors due to the 24 second difference +# between UTC and GMT when using some zone files (eg. the PDT zone from +# the US) +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.auth import system_session +from samba.samdb import SamDB +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 + """ + def __init__(self, samdb): + """Initializes the partitions class which can hold + our local DCs partitions or all the partitions in + the forest + """ + self.dsa_table = {} # dsa objects + self.part_table = {} # partition objects + self.site_table = {} + self.my_dsa_dnstr = None # My dsa DN + self.my_site_dnstr = None + self.samdb = samdb + return + + def load_my_site(self): + """Loads the Site class for the local DSA + Raises an Exception on error + """ + self.my_site_dnstr = "CN=%s,CN=Sites,%s" % (samdb.server_site_name(), + samdb.get_config_basedn()) + site = Site(self.my_site_dnstr) + + site.load_site(samdb) + self.site_table[self.my_site_dnstr] = site + return + + def load_my_dsa(self): + """Discover my nTDSDSA thru the rootDSE entry and + instantiate and load the DSA. The dsa is inserted + into the dsa_table by dn string + Raises an Exception on error. + """ + dn = ldb.Dn(self.samdb, "") + try: + res = samdb.search(base=dn, scope=ldb.SCOPE_BASE, + attrs=["dsServiceName"]) + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find my nTDSDSA - (%s)" % estr) + return + + dnstr = res[0]["dsServiceName"][0] + + # already loaded + if dnstr in self.dsa_table.keys(): + return + + self.my_dsa_dnstr = dnstr + dsa = DirectoryServiceAgent(dnstr) + + dsa.load_dsa(samdb) + + # Assign this dsa to my dsa table + # and index by dsa dn + self.dsa_table[dnstr] = dsa + + return + + def load_all_dsa(self): + """Discover all nTDSDSA thru the sites entry and + instantiate and load the DSAs. Each dsa is inserted + into the dsa_table by dn string. + Raises an Exception on error. + """ + try: + res = self.samdb.search("CN=Sites,%s" % + self.samdb.get_config_basedn(), + scope=ldb.SCOPE_SUBTREE, + expression="(objectClass=nTDSDSA)") + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find nTDSDSAs - (%s)" % estr) + return + + for msg in res: + dnstr = str(msg.dn) + + # already loaded + if dnstr in self.dsa_table.keys(): + continue + + dsa = DirectoryServiceAgent(dnstr) + + dsa.load_dsa(self.samdb) + + # Assign this dsa to my dsa table + # and index by dsa dn + self.dsa_table[dnstr] = dsa + + return + + def load_all_partitions(self): + """Discover all NCs thru the Partitions dn and + 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 + """ + try: + res = self.samdb.search("CN=Partitions,%s" % + self.samdb.get_config_basedn(), + scope=ldb.SCOPE_SUBTREE, + expression="(objectClass=crossRef)") + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find partitions - (%s)" % estr) + + for msg in res: + partstr = str(msg.dn) + + # already loaded + if partstr in self.part_table.keys(): + continue + + part = Partition(partstr) + + part.load_partition(self.samdb) + self.part_table[partstr] = part + + return + + def should_be_present_test(self): + """Enumerate all loaded partitions and DSAs and test + if NC should be present as replica + """ + for partdn, part in self.part_table.items(): + + for dsadn, dsa in self.dsa_table.items(): + needed, ro, partial = part.should_be_present(dsa) + + logger.info("dsadn:%s\nncdn:%s\nneeded=%s:ro=%s:partial=%s\n" % \ + (dsa.dsa_dnstr, part.nc_dnstr, needed, ro, partial)) + return + + def refresh_failed_links_connections(self): + # XXX - not implemented yet + return + + 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. + """ + # XXX - not implemented yet + return False + + def remove_unneeded_failed_links_connections(self): + # XXX - not implemented yet + return + + def remove_unneeded_ntds_connections(self): + # XXX - not implemented yet + return + + def translate_connections(self): + # XXX - not implemented yet + return + + def intersite(self): + """The head method for generating the inter-site KCC replica + connection graph and attendant nTDSConnection objects + in the samdb + """ + # XXX - not implemented yet + return + + def update_rodc_connection(self): + """Runs when the local DC is an RODC and updates the RODC NTFRS + connection object. + """ + # Given an nTDSConnection object cn1, such that cn1.options contains + # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2, + # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure + # that the following is true: + # + # cn1.fromServer = cn2.fromServer + # cn1.schedule = cn2.schedule + # + # If no such cn2 can be found, cn1 is not modified. + # If no such cn1 can be found, nothing is modified by this task. + + # 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 + """ + n = 0 + while True: + if node_count <= (2 * (n * n) + (6 * n) + 7): + break + n = n + 1 + n = n + 2 + if n < 50: + return n + return 50 + + def construct_intrasite_graph(self, site_local, dc_local, + nc_x, gc_only, detect_stale): + + # We're using the MS notation names here to allow + # correlation back to the published algorithm. + # + # nc_x - naming context (x) that we are testing if it + # "should be present" on the local DC + # f_of_x - replica (f) found on a DC (s) for NC (x) + # dc_s - DC where f_of_x replica was found + # dc_local - local DC that potentially needs a replica + # (f_of_x) + # r_list - replica list R + # p_of_x - replica (p) is partial and found on a DC (s) + # for NC (x) + # l_of_x - replica (l) is the local replica for NC (x) + # that should appear on the local DC + # r_len = is length of replica list |R| + # + # If the DSA doesn't need a replica for this + # partition (NC x) then continue + needed, ro, partial = nc_x.should_be_present(dc_local) + + logger.debug("construct_intrasite_graph:\n" + \ + "nc_x: %s\ndc_local: %s\n" % \ + (nc_x, dc_local) + \ + "gc_only: %s\nneeded: %s\nro: %s\npartial: %s" % \ + (gc_only, needed, ro, partial)) + + if needed == False: + return + + # 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) + + l_of_x.identify_by_basedn(self.samdb) + + l_of_x.rep_partial = partial + l_of_x.rep_ro = ro + + # Empty replica sequence list + r_list = [] + + # We'll loop thru all the DSAs looking for + # writeable NC replicas that match the naming + # context dn for (nc_x) + # + for dc_s_dn, dc_s in self.dsa_table.items(): + + # If this partition (nc_x) doesn't appear as a + # replica (f_of_x) on (dc_s) then continue + if not nc_x.nc_dnstr in dc_s.rep_table.keys(): + continue + + # Pull out the NCReplica (f) of (x) with the dn + # that matches NC (x) we are examining. + f_of_x = dc_s.rep_table[nc_x.nc_dnstr] + + # Replica (f) of NC (x) must be writable + if f_of_x.is_ro() == True: + continue + + # Replica (f) of NC (x) must satisfy the + # "is present" criteria for DC (s) that + # it was found on + if f_of_x.is_present() == False: + continue + + # DC (s) must be a writable DSA other than + # my local DC. In other words we'd only replicate + # from other writable DC + if dc_s.is_ro() or dc_s is dc_local: + continue + + # Certain replica graphs are produced only + # for global catalogs, so test against + # method input parameter + if gc_only and dc_s.is_gc() == False: + continue + + # DC (s) must be in the same site as the local DC + # This is the intra-site algorithm. We are not + # replicating across multiple sites + if site_local.is_same_site(dc_s) == False: + continue + + # If NC (x) is intended to be read-only full replica + # for a domain NC on the target DC then the source + # DC should have functional level at minimum WIN2008 + # + # Effectively we're saying that in order to replicate + # to a targeted RODC (which was introduced in Windows 2008) + # then we have to replicate from a DC that is also minimally + # at that level. + # + # You can also see this requirement in the MS special + # considerations for RODC which state that to deploy + # an RODC, at least one writable domain controller in + # the domain must be running Windows Server 2008 + if ro and partial == False and nc_x.nc_type == NCType.domain: + if dc_s.is_minimum_behavior(DS_BEHAVIOR_WIN2008) == False: + continue + + # If we haven't been told to turn off stale connection + # detection and this dsa has a stale connection then + # continue + if detect_stale and self.is_stale_link_connection(dc_s) == True: + continue + + # Replica meets criteria. Add it to table indexed + # by the GUID of the DC that it appears on + r_list.append(f_of_x) + + # If a partial (not full) replica of NC (x) "should be present" + # on the local DC, append to R each partial replica (p of x) + # such that p "is present" on a DC satisfying the same + # criteria defined above for full replica DCs. + if partial == True: + + # Now we loop thru all the DSAs looking for + # partial NC replicas that match the naming + # context dn for (NC x) + for dc_s_dn, dc_s in self.dsa_table.items(): + + # If this partition NC (x) doesn't appear as a + # replica (p) of NC (x) on the dsa DC (s) then + # continue + if not nc_x.nc_dnstr in dc_s.rep_table.keys(): + continue + + # Pull out the NCReplica with the dn that + # matches NC (x) we are examining. + p_of_x = dsa.rep_table[nc_x.nc_dnstr] + + # Replica (p) of NC (x) must be partial + if p_of_x.is_partial() == False: + continue + + # Replica (p) of NC (x) must satisfy the + # "is present" criteria for DC (s) that + # it was found on + if p_of_x.is_present() == False: + continue + + # DC (s) must be a writable DSA other than + # my DSA. In other words we'd only replicate + # from other writable DSA + if dc_s.is_ro() or dc_s is dc_local: + continue + + # Certain replica graphs are produced only + # for global catalogs, so test against + # method input parameter + if gc_only and dc_s.is_gc() == False: + continue + + # DC (s) must be in the same site as the local DC + # This is the intra-site algorithm. We are not + # replicating across multiple sites + if site_local.is_same_site(dc_s) == False: + continue + + # This criteria is moot (a no-op) for this case + # because we are scanning for (partial = True). The + # MS algorithm statement says partial replica scans + # should adhere to the "same" criteria as full replica + # scans so the criteria doesn't change here...its just + # rendered pointless. + # + # The case that is occurring would be a partial domain + # replica is needed on a local DC global catalog. There + # is no minimum windows behavior for those since GCs + # have always been present. + if ro and partial == False and nc_x.nc_type == NCType.domain: + if dc_s.is_minimum_behavior(DS_BEHAVIOR_WIN2008) == False: + continue + + # If we haven't been told to turn off stale connection + # detection and this dsa has a stale connection then + # continue + if detect_stale and self.is_stale_link_connection(dc_s) == True: + continue + + # Replica meets criteria. Add it to table indexed + # by the GUID of the DSA that it appears on + r_list.append(p_of_x) + + # Append to R the NC replica that "should be present" + # on the local DC + r_list.append(l_of_x) + + r_list.sort(sort_replica_by_dsa_guid) + + r_len = len(r_list) + + max_node_edges = self.intrasite_max_node_edges(r_len) + + # Add a node for each r_list element to the replica graph + graph_list = [] + for rep in r_list: + node = GraphNode(rep.rep_dsa_dnstr, max_node_edges) + graph_list.append(node) + + # For each r(i) from (0 <= i < |R|-1) + i = 0 + while i < (r_len-1): + # Add an edge from r(i) to r(i+1) if r(i) is a full + # replica or r(i+1) is a partial replica + if r_list[i].is_partial() == False or \ + r_list[i+1].is_partial() == True: + graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr) + + # Add an edge from r(i+1) to r(i) if r(i+1) is a full + # replica or ri is a partial replica. + if r_list[i+1].is_partial() == False or \ + r_list[i].is_partial() == True: + graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr) + i = i + 1 + + # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica + # or r0 is a partial replica. + if r_list[r_len-1].is_partial() == False or \ + r_list[0].is_partial() == True: + graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr) + + # Add an edge from r0 to r|R|-1 if r0 is a full replica or + # r|R|-1 is a partial replica. + if r_list[0].is_partial() == False or \ + r_list[r_len-1].is_partial() == True: + graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr) + + # For each existing nTDSConnection object implying an edge + # from rj of R to ri such that j != i, an edge from rj to ri + # is not already in the graph, and the total edges directed + # to ri is less than n+2, the KCC adds that edge to the graph. + i = 0 + while i < r_len: + dsa = self.dsa_table[graph_list[i].dsa_dnstr] + graph_list[i].add_edges_from_connections(dsa) + i = i + 1 + + i = 0 + while i < r_len: + tnode = graph_list[i] + + # To optimize replication latency in sites with many NC replicas, the + # KCC adds new edges directed to ri to bring the total edges to n+2, + # where the NC replica rk of R from which the edge is directed + # is chosen at random such that k != i and an edge from rk to ri + # is not already in the graph. + # + # Note that the KCC tech ref does not give a number for the definition + # of "sites with many NC replicas". At a bare minimum to satisfy + # n+2 edges directed at a node we have to have at least three replicas + # in |R| (i.e. if n is zero then at least replicas from two other graph + # nodes may direct edges to us). + if r_len >= 3: + # pick a random index + findex = rindex = random.randint(0, r_len-1) + + # while this node doesn't have sufficient edges + while tnode.has_sufficient_edges() == False: + # If this edge can be successfully added (i.e. not + # the same node and edge doesn't already exist) then + # select a new random index for the next round + if tnode.add_edge_from(graph_list[rindex].dsa_dnstr) == True: + findex = rindex = random.randint(0, r_len-1) + else: + # Otherwise continue looking against each node + # after the random selection + rindex = rindex + 1 + if rindex >= r_len: + rindex = 0 + + if rindex == findex: + logger.error("Unable to satisfy max edge criteria!") + break + + # Print the graph node in debug mode + logger.debug("%s" % tnode) + + # For each edge directed to the local DC, ensure a nTDSConnection + # points to us that satisfies the KCC criteria + if graph_list[i].dsa_dnstr == dc_local.dsa_dnstr: + graph_list[i].add_connections_from_edges(dc_local) + + i = i + 1 + + return + + def intrasite(self): + """The head method for generating the intra-site KCC replica + connection graph and attendant nTDSConnection objects + in the samdb + """ + # Retrieve my DSA + mydsa = self.dsa_table[self.my_dsa_dnstr] + + logger.debug("intrasite enter:\nmydsa: %s" % mydsa) + + # 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() + + # Loop thru all the partitions. + for partdn, part in self.part_table.items(): + self.construct_intrasite_graph(mysite, mydsa, part, \ + False, \ + detect_stale) + + # If the DC is a GC server, the KCC constructs an additional NC + # replica graph (and creates nTDSConnection objects) for the + # config NC as above, except that only NC replicas that "are present" + # on GC servers are added to R. + for partdn, part in self.part_table.items(): + if part.is_config(): + self.construct_intrasite_graph(mysite, mydsa, part, \ + True, \ + detect_stale) + + # The DC repeats the NC replica graph computation and nTDSConnection + # creation for each of the NC replica graphs, this time assuming + # that no DC has failed. It does so by re-executing the steps as + # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were + # set in the options attribute of the site settings object for + # the local DC's site. (ie. we set "detec_stale" flag to False) + + # Loop thru all the partitions. + for partdn, part in self.part_table.items(): + self.construct_intrasite_graph(mysite, mydsa, part, \ + False, \ + False) # don't detect stale + + # If the DC is a GC server, the KCC constructs an additional NC + # replica graph (and creates nTDSConnection objects) for the + # config NC as above, except that only NC replicas that "are present" + # on GC servers are added to R. + for partdn, part in self.part_table.items(): + if part.is_config(): + self.construct_intrasite_graph(mysite, mydsa, part, \ + True, \ + False) # don't detect stale + + # Commit any newly created connections to the samdb + mydsa.commit_connection_table(self.samdb) + + logger.debug("intrasite exit:\nmydsa: %s" % mydsa) + return + + def run(self): + """Method to perform a complete run of the KCC and + produce an updated topology for subsequent NC replica + syncronization between domain controllers + """ + # Setup + try: + self.load_my_dsa() + self.load_all_dsa() + self.load_all_partitions() + self.load_my_site() + + except Exception, estr: + logger.error("%s" % estr) + return + + # self.should_be_present_test() + + # These are the published steps (in order) for the + # MS description of the KCC algorithm + + # Step 1 + self.refresh_failed_links_connections() + + # Step 2 + self.intrasite() + + # Step 3 + self.intersite() + + # Step 4 + self.remove_unneeded_ntds_connections() + + # Step 5 + self.translate_connections() + + # Step 6 + self.remove_unneeded_failed_links_connections() + + # Step 7 + self.update_rodc_connection() + + return + +################################################## +# Global Functions +################################################## +def sort_replica_by_dsa_guid(rep1, rep2): + return cmp(rep1.rep_dsa_guid, rep2.rep_dsa_guid) + +################################################## +# kcccompute entry point +################################################## + +parser = optparse.OptionParser("kcccompute [options]") +sambaopts = options.SambaOptions(parser) +credopts = options.CredentialsOptions(parser) + +parser.add_option_group(sambaopts) +parser.add_option_group(credopts) +parser.add_option_group(options.VersionOptions(parser)) + +parser.add_option("--debug", help="debug output", action="store_true") +parser.add_option("--seed", help="random number seed") + +logger = logging.getLogger("kcccompute") +logger.addHandler(logging.StreamHandler(sys.stdout)) + +lp = sambaopts.get_loadparm() +creds = credopts.get_credentials(lp, fallback_machine=True) + +opts, args = parser.parse_args() + +if opts.debug: + logger.setLevel(logging.DEBUG) +else: + logger.setLevel(logging.WARNING) + +# initialize seed from optional input parameter +if opts.seed: + random.seed(int(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) + +# Instantiate Knowledge Consistency Checker and perform run +kcc = KCC(samdb) +kcc.run() diff --git a/source4/scripting/bin/wscript_build b/source4/scripting/bin/wscript_build index 87387054b3..ad83894c8c 100644 --- a/source4/scripting/bin/wscript_build +++ b/source4/scripting/bin/wscript_build @@ -2,5 +2,6 @@ bld.SAMBA_SCRIPT('samba_dnsupdate', pattern='samba_dnsupdate', installdir='.') bld.SAMBA_SCRIPT('samba_spnupdate', pattern='samba_spnupdate', installdir='.') +bld.SAMBA_SCRIPT('samba_kcc', pattern='samba_kcc', installdir='.') bld.SAMBA_SCRIPT('upgradeprovision', pattern='upgradeprovision', installdir='.') bld.SAMBA_SCRIPT('samba-tool', pattern='samba-tool', installdir='.') diff --git a/source4/scripting/wscript_build b/source4/scripting/wscript_build index 90f1f2f021..8029187fff 100644 --- a/source4/scripting/wscript_build +++ b/source4/scripting/wscript_build @@ -2,7 +2,7 @@ from samba_utils import MODE_755 -bld.INSTALL_FILES('${SBINDIR}','bin/upgradeprovision bin/samba_dnsupdate bin/samba_spnupdate bin/samba-tool', +bld.INSTALL_FILES('${SBINDIR}','bin/upgradeprovision bin/samba_dnsupdate bin/samba_spnupdate bin/samba-tool bin/samba_kcc', chmod=MODE_755, python_fixup=True, flat=True) bld.RECURSE('bin') |